tksbrokerapi.TKSBrokerAPI

TKSBrokerAPI is a python API to work with some methods of Tinkoff Open API using REST protocol. It can view history, orders and market information. Also, you can open orders and trades.

If you run this module as CLI program then it realizes simple logic: receiving a lot of options and execute one command. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples

Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html

About Tinkoff Invest API: https://tinkoff.github.io/investAPI/

Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/

   1# -*- coding: utf-8 -*-
   2# Author: Timur Gilmullin
   3
   4"""
   5**TKSBrokerAPI** is a python API to work with some methods of Tinkoff Open API using REST protocol.
   6It can view history, orders and market information. Also, you can open orders and trades.
   7
   8If you run this module as CLI program then it realizes simple logic: receiving a lot of options and execute one command.
   9**See examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
  10
  11**Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
  12
  13About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
  14
  15Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
  16"""
  17
  18# Copyright (c) 2022 Gilmillin Timur Mansurovich
  19#
  20# Licensed under the Apache License, Version 2.0 (the "License");
  21# you may not use this file except in compliance with the License.
  22# You may obtain a copy of the License at
  23#
  24#     http://www.apache.org/licenses/LICENSE-2.0
  25#
  26# Unless required by applicable law or agreed to in writing, software
  27# distributed under the License is distributed on an "AS IS" BASIS,
  28# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  29# See the License for the specific language governing permissions and
  30# limitations under the License.
  31
  32
  33import sys
  34import os
  35from argparse import ArgumentParser
  36from importlib.metadata import version
  37
  38from datetime import datetime, timedelta
  39from dateutil.tz import tzlocal, tzutc
  40from time import sleep
  41
  42import re
  43import json
  44import requests
  45import traceback as tb
  46from typing import Union
  47
  48from multiprocessing import cpu_count
  49from multiprocessing.pool import ThreadPool
  50import pandas as pd
  51
  52from TKSEnums import *  # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/
  53
  54from pricegenerator.PriceGenerator import PriceGenerator, uLogger  # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator
  55from pricegenerator.UniLogger import DisableLogger as PGDisLog  # Method for disable log from PriceGenerator
  56
  57import UniLogger as uLog  # Logger for TKSBrokerAPI
  58
  59
  60# --- Common technical parameters:
  61
  62PGDisLog(uLogger.handlers[0])  # Disable 3-rd party logging from PriceGenerator
  63uLogger = uLog.UniLogger  # init logger for TKSBrokerAPI
  64uLogger.level = 10  # debug level by default for TKSBrokerAPI module
  65uLogger.handlers[0].level = 20  # info level by default for STDOUT of TKSBrokerAPI module
  66
  67__version__ = "1.4"  # The "major.minor" version setup here, but build number define at the build-server only
  68
  69CPU_COUNT = cpu_count()  # host's real CPU count
  70CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1  # how many CPUs will be used for parallel calculations
  71
  72# --- Main constants:
  73
  74NANO = 0.000000001  # SI-constant nano = 10^-9
  75
  76
  77def NanoToFloat(units: str, nano: int) -> float:
  78    """
  79    Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples:
  80
  81    `NanoToFloat(units="2", nano=500000000) -> 2.5`
  82
  83    `NanoToFloat(units="0", nano=50000000) -> 0.05`
  84
  85    :param units: integer string or integer parameter that represents the integer part of number
  86    :param nano: integer string or integer parameter that represents the fractional part of number
  87    :return: float view of number
  88    """
  89    return int(units) + int(nano) * NANO
  90
  91
  92def FloatToNano(number: float) -> dict:
  93    """
  94    Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples:
  95
  96    `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}`
  97
  98    `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}`
  99
 100    :param number: float number
 101    :return: nano-type view of number: `{"units": "string", "nano": integer}`
 102    """
 103    splitByPoint = str(number).split(".")
 104    frac = 0
 105
 106    if len(splitByPoint) > 1:
 107        if len(splitByPoint[1]) <= 9:
 108            frac = int("{}{}".format(
 109                int(splitByPoint[1]),
 110                "0" * (9 - len(splitByPoint[1])),
 111            ))
 112
 113    if (number < 0) and (frac > 0):
 114        frac = -frac
 115
 116    return {"units": str(int(number)), "nano": frac}
 117
 118
 119def GetDatesAsString(start: str = None, end: str = None) -> tuple:
 120    """
 121    Create tuple of date and time strings with timezone parsed from user-friendly date.
 122
 123    User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020).
 124
 125    Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")
 126    An error exception will occur if input date has incorrect format.
 127
 128    If `start=None`, `end=None` then return dates from yesterday to the end of the day.
 129    If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day.
 130    If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`.
 131    Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago.
 132
 133    Also, you can use keywords for start if `end=None`:
 134    `today` (from 00:00:00 to the end of current day),
 135    `yesterday` (-1 day from 00:00:00 to 23:59:59),
 136    `week` (-7 day from 00:00:00 to the end of current day),
 137    `month` (-30 day from 00:00:00 to the end of current day),
 138    `year` (-365 day from 00:00:00 to the end of current day),
 139
 140    :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI.
 141             See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`.
 142             Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day.
 143    """
 144    uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end))
 145    s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0)  # start of the current day
 146    e = s.replace(hour=23, minute=59, second=59, microsecond=0)  # end of the current day
 147
 148    # time between start and the end of the current day:
 149    if start is None or start.lower() == "today":
 150        pass
 151
 152    # from start of the last day to the end of the last day:
 153    elif start.lower() == "yesterday":
 154        s -= timedelta(days=1)
 155        e -= timedelta(days=1)
 156
 157    # week (-7 day from 00:00:00 to the end of the current day):
 158    elif start.lower() == "week":
 159        s -= timedelta(days=6)  # +1 current day already taken into account
 160
 161    # month (-30 day from 00:00:00 to the end of current day):
 162    elif start.lower() == "month":
 163        s -= timedelta(days=29)  # +1 current day already taken into account
 164
 165    # year (-365 day from 00:00:00 to the end of current day):
 166    elif start.lower() == "year":
 167        s -= timedelta(days=364)  # +1 current day already taken into account
 168
 169    # -N days ago to the end of current day:
 170    elif start.startswith('-') and start[1:].isdigit():
 171        s -= timedelta(days=abs(int(start)) - 1)  # +1 current day already taken into account
 172
 173    # dates between start day at 00:00:00 and the end of the last day at 23:59:59:
 174    else:
 175        s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc())
 176        e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e
 177
 178    # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API:
 179    s = s.strftime(TKS_DATE_TIME_FORMAT)
 180    e = e.strftime(TKS_DATE_TIME_FORMAT)
 181
 182    uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e))
 183
 184    return s, e
 185
 186
 187class TinkoffBrokerServer:
 188    """
 189    This class implements methods to work with Tinkoff broker server.
 190
 191    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
 192
 193    About `token`: https://tinkoff.github.io/investAPI/token/
 194    """
 195    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 196        """
 197        Main class init.
 198
 199        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 200        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 201                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 202        :param useCache: use default cache file with raw data to use instead of `iList`.
 203                         True by default. Cache is auto-update if new day has come.
 204                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 205        :param defaultCache: path to default cache file. `dump.json` by default.
 206        """
 207        if token is None or not token:
 208            try:
 209                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 210                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 211
 212            except KeyError:
 213                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 214                raise Exception("Token required")
 215
 216        else:
 217            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 218            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 219
 220        if accountId is None or not accountId:
 221            try:
 222                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 223                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 224
 225            except KeyError:
 226                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 227
 228        else:
 229            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 230            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 231
 232        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 233        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 234
 235        Latest version: https://pypi.org/project/tksbrokerapi/
 236        """
 237
 238        self.aliases = TKS_TICKER_ALIASES
 239        """Some aliases instead official tickers.
 240
 241        See also: `TKSEnums.TKS_TICKER_ALIASES`
 242        """
 243
 244        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 245
 246        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 247
 248        self.ticker = ""
 249        """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 250
 251        See also: `SearchByTicker()`, `SearchInstruments()`.
 252        """
 253
 254        self.figi = ""
 255        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`.
 256
 257        See also: `SearchByFIGI()`, `SearchInstruments()`.
 258        """
 259
 260        self.depth = 1
 261        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 262
 263        See also: `GetCurrentPrices()`.
 264        """
 265
 266        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 267        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 268
 269        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 270        """
 271
 272        uLogger.debug("Broker API server: {}".format(self.server))
 273
 274        self.timeout = 15
 275        """Server operations timeout in seconds. Default: `15`.
 276
 277        See also: `SendAPIRequest()`.
 278        """
 279
 280        self.headers = {
 281            "Content-Type": "application/json",
 282            "accept": "application/json",
 283            "Authorization": "Bearer {}".format(self.token),
 284            "x-app-name": "Tim55667757.TKSBrokerAPI",
 285        }
 286        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 287
 288        See also: `SendAPIRequest()`.
 289        """
 290
 291        self.body = None
 292        """Request body which send to broker server. Default: `None`.
 293
 294        See also: `SendAPIRequest()`.
 295        """
 296
 297        self.historyFile = None
 298        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only pandas dataframe.
 299
 300        See also: `History()`.
 301        """
 302
 303        self.htmlHistoryFile = "index.html"
 304        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 305
 306        See also: `ShowHistoryChart()`.
 307        """
 308
 309        self.instrumentsFile = "instruments.md"
 310        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 311
 312        See also: `ShowInstrumentsInfo()`.
 313        """
 314
 315        self.searchResultsFile = "search-results.md"
 316        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 317
 318        See also: `SearchInstruments()`.
 319        """
 320
 321        self.pricesFile = "prices.md"
 322        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 323
 324        See also: `GetListOfPrices()`.
 325        """
 326
 327        self.infoFile = "info.md"
 328        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 329
 330        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 331        """
 332
 333        self.bondsXLSXFile = "ext-bonds.xlsx"
 334        """Filename where wider pandas dataframe with more information about bonds: main info, current prices, 
 335        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 336
 337        See also: `ExtendBondsData()`.
 338        """
 339
 340        self.calendarFile = "calendar.md"
 341        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 342        
 343        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 344
 345        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 346        """
 347
 348        self.overviewFile = "overview.md"
 349        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 350
 351        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 352        """
 353
 354        self.overviewDigestFile = "overview-digest.md"
 355        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 356
 357        See also: `Overview()` with parameter `details="digest"`.
 358        """
 359
 360        self.overviewPositionsFile = "overview-positions.md"
 361        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 362
 363        See also: `Overview()` with parameter `details="positions"`.
 364        """
 365
 366        self.overviewOrdersFile = "overview-orders.md"
 367        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 368
 369        See also: `Overview()` with parameter `details="orders"`.
 370        """
 371
 372        self.overviewAnalyticsFile = "overview-analytics.md"
 373        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 374
 375        See also: `Overview()` with parameter `details="analytics"`.
 376        """
 377
 378        self.reportFile = "deals.md"
 379        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 380
 381        See also: `Deals()`.
 382        """
 383
 384        self.withdrawalLimitsFile = "limits.md"
 385        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 386
 387        See also: `OverviewLimits()` and `RequestLimits()`.
 388        """
 389
 390        self.userInfoFile = "user-info.md"
 391        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 392
 393        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 394        """
 395
 396        self.userAccountsFile = "accounts.md"
 397        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 398
 399        See also: `OverviewAccounts()`, `RequestAccounts()`.
 400        """
 401
 402        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 403        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 404
 405        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 406
 407        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 408        """
 409
 410        self.iList = None  # init iList for raw instruments data
 411        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 412        
 413        See also: `Listing()`, `DumpInstruments()`.
 414        """
 415
 416        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 417        if useCache:
 418            if os.path.exists(self.iListDumpFile):
 419                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 420                curTime = datetime.now(tzutc())
 421
 422                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 423                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 424
 425                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 426
 427                else:
 428                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 429
 430                    uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile)))
 431                    uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 432
 433            else:
 434                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 435                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 436
 437        else:
 438            self.iList = self.Listing()  # request new raw instruments data from broker server
 439            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 440
 441        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 442        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 443
 444        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 445        """
 446
 447    @staticmethod
 448    def _ParseJSON(rawData="{}", debug: bool = False) -> dict:
 449        """
 450        Parse JSON from response string.
 451
 452        :param rawData: this is a string with JSON-formatted text.
 453        :param debug: if `True` then print more debug information.
 454        :return: JSON (dictionary), parsed from server response string.
 455        """
 456        if debug:
 457            uLogger.debug("Raw text body:")
 458            uLogger.debug(rawData)
 459
 460        responseJSON = json.loads(rawData) if rawData else {}
 461
 462        if debug:
 463            uLogger.debug("JSON formatted:")
 464            for jsonLine in json.dumps(responseJSON, indent=4).split('\n'):
 465                uLogger.debug(jsonLine)
 466
 467        return responseJSON
 468
 469    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict:
 470        """
 471        Send GET or POST request to broker server and receive JSON object.
 472
 473        self.header: must be defining with dictionary of headers.
 474        self.body: if define then used as request body. None by default.
 475        self.timeout: global request timeout, 15 seconds by default.
 476        :param url: url with REST request.
 477        :param reqType: send "GET" or "POST" request. "GET" by default.
 478        :param retry: how many times retry after first request if an 5xx server errors occurred.
 479        :param pause: sleep time in seconds between retries.
 480        :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc.
 481        :return: response JSON (dictionary) from broker.
 482        """
 483        if reqType not in ("GET", "POST"):
 484            uLogger.error("You can define request type: 'GET' or 'POST'!")
 485            raise Exception("Incorrect value")
 486
 487        if debug:
 488            uLogger.debug("Request parameters:")
 489            uLogger.debug("    - REST API URL: {}".format(url))
 490            uLogger.debug("    - request type: {}".format(reqType))
 491            uLogger.debug("    - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***")))
 492            uLogger.debug("    - body: {}".format(self.body))
 493
 494        # fast hack to avoid all operations with some tickers/FIGI
 495        responseJSON = {}
 496        oK = True
 497        for item in self.exclude:
 498            if item in url:
 499                if debug:
 500                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 501
 502                oK = False
 503                break
 504
 505        if oK:
 506            counter = 0
 507            response = None
 508            errMsg = ""
 509
 510            while not response and counter <= retry:
 511                if reqType == "GET":
 512                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 513
 514                if reqType == "POST":
 515                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 516
 517                if debug:
 518                    uLogger.debug("Response:")
 519                    uLogger.debug("    - status code: {}".format(response.status_code))
 520                    uLogger.debug("    - reason: {}".format(response.reason))
 521                    uLogger.debug("    - body length: {}".format(len(response.text)))
 522                    uLogger.debug("    - headers: {}".format(response.headers))
 523
 524                # Server returns some headers:
 525                # - `x-ratelimit-limit` - shows the settings of the current user limit for this method.
 526                # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute.
 527                # - `x-ratelimit-reset` - time in seconds before resetting the request counter.
 528                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 529                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 530                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 531                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 532                    sleep(rateLimitWait)
 533
 534                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 535                if 400 <= response.status_code < 500:
 536                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 537                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 538                    counter = retry + 1
 539
 540                if 500 <= response.status_code < 600:
 541                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 542                    uLogger.debug("    - not oK, {}".format(errMsg))
 543                    counter += 1
 544
 545                    if counter <= retry:
 546                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 547                        sleep(pause)
 548
 549            responseJSON = self._ParseJSON(response.text)
 550
 551            if errMsg:
 552                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 553                uLogger.error("    - not oK, {}".format(errMsg))
 554
 555        return responseJSON
 556
 557    def _IUpdater(self, iType: str) -> tuple:
 558        """
 559        Request instrument by type from server. See available API methods for instruments:
 560        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 561        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 562        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 563        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 564        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 565
 566        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 567        :return: tuple with iType name and list of available instruments of current type for defined user token.
 568        """
 569        result = []
 570
 571        if iType in TKS_INSTRUMENTS:
 572            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 573
 574            # all instruments have the same body in API v2 requests:
 575            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 576            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 577            result = self.SendAPIRequest(instrumentURL, reqType="POST", debug=False)["instruments"]
 578
 579        return iType, result
 580
 581    def _IWrapper(self, kwargs):
 582        """
 583        Wrapper runs instrument's update method `_IUpdater()`.
 584        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 585        """
 586        return self._IUpdater(**kwargs)
 587
 588    def Listing(self) -> dict:
 589        """
 590        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 591
 592        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 593        """
 594        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 595        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 596
 597        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 598        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 599        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 600
 601        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 602        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 603        poolUpdater.close()
 604
 605        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 606        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 607        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 608
 609        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 610        for iType in iList.keys():
 611            for ticker in iList[iType]:
 612                iList[iType][ticker]["type"] = iType
 613
 614                if "minPriceIncrement" in iList[iType][ticker].keys():
 615                    iList[iType][ticker]["step"] = NanoToFloat(
 616                        iList[iType][ticker]["minPriceIncrement"]["units"],
 617                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 618                    )
 619
 620                else:
 621                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 622
 623        return iList
 624
 625    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 626        """
 627        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 628
 629        See also: `DumpInstruments()`, `Listing()`.
 630
 631        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 632                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 633        """
 634        if self.iListDumpFile is None or not self.iListDumpFile:
 635            uLogger.error("Output name of dump file must be defined!")
 636            raise Exception("Filename required")
 637
 638        if not self.iList or forceUpdate:
 639            self.iList = self.Listing()
 640
 641        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 642
 643        # Save as XLSX with separated sheets for every type of instruments:
 644        with pd.ExcelWriter(
 645                path=xlsxDumpFile,
 646                date_format=TKS_DATE_FORMAT,
 647                datetime_format=TKS_DATE_TIME_FORMAT,
 648                mode="w",
 649        ) as writer:
 650            for iType in TKS_INSTRUMENTS:
 651                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 652                df = df[sorted(df)]  # sorted by column names
 653                df = df.applymap(
 654                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 655                    na_action="ignore",
 656                )  # converting numbers from nano-type to float in every cell
 657                df.to_excel(
 658                    writer,
 659                    sheet_name=iType,
 660                    encoding="UTF-8",
 661                    freeze_panes=(1, 1),
 662                )  # saving as XLSX-file with freeze first row and column as headers
 663
 664        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 665
 666    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 667        """
 668        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 669        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 670
 671        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 672
 673        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 674                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 675        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 676        """
 677        if self.iListDumpFile is None or not self.iListDumpFile:
 678            uLogger.error("Output name of dump file must be defined!")
 679            raise Exception("Filename required")
 680
 681        if not self.iList or forceUpdate:
 682            self.iList = self.Listing()
 683
 684        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 685        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 686            fH.write(jsonDump)
 687
 688        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 689
 690        return jsonDump
 691
 692    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 693        """
 694        Show information about one instrument defined by json data and prints it in Markdown format.
 695
 696        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 697
 698        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
 699        :param show: if `True` then also printing information about instrument and its current price.
 700        :return: multilines text in Markdown format with information about one instrument.
 701        """
 702        splitLine = "|                                                             |                                                        |\n"
 703        infoText = ""
 704
 705        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 706            info = [
 707                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
 708                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
 709                "| Parameters                                                  | Values                                                 |\n",
 710                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 711                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 712                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 713            ]
 714
 715            if "sector" in iJSON.keys() and iJSON["sector"]:
 716                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 717
 718            info.append("| Country of instrument:                                      | {:<54} |\n".format("{}{}".format(
 719                "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "",
 720                iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "",
 721            )))
 722
 723            info.extend([
 724                splitLine,
 725                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 726                "| Exchange:                                                   | {:<54} |\n".format(iJSON["exchange"]),
 727            ])
 728
 729            if "isin" in iJSON.keys() and iJSON["isin"]:
 730                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 731
 732            if "classCode" in iJSON.keys():
 733                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 734
 735            info.extend([
 736                splitLine,
 737                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 738                splitLine,
 739                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 740                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 741                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 742            ])
 743
 744            if iJSON["figi"]:
 745                self.figi = iJSON["figi"]
 746                iJSON = iJSON | self.RequestTradingStatus()
 747
 748                info.extend([
 749                    splitLine,
 750                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 751                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 752                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 753                ])
 754
 755            info.append(splitLine)
 756
 757            if "type" in iJSON.keys() and iJSON["type"]:
 758                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 759
 760            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 761                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 762
 763            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 764                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 765
 766            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 767                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 768
 769            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 770                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 771
 772            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 773                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 774
 775            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 776                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 777
 778            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 779                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 780
 781            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 782                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 783
 784            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 785                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 786
 787            if "currency" in iJSON.keys():
 788                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 789
 790            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 791                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 792
 793            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 794                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 795
 796            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 797                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 798
 799            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 800                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 801
 802            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 803                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 804
 805            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 806                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 807
 808            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 809                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 810
 811            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 812                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 813
 814            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 815                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 816
 817            iExt = None
 818            if iJSON["type"] == "Bonds":
 819                info.extend([
 820                    splitLine,
 821                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 822                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 823                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 824                        iJSON["nominal"]["currency"],
 825                    )),
 826                ])
 827
 828                if "floatingCouponFlag" in iJSON.keys():
 829                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 830
 831                if "amortizationFlag" in iJSON.keys():
 832                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 833
 834                info.append(splitLine)
 835
 836                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 837                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 838
 839                iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 840
 841                info.extend([
 842                    "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 843                    "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 844                    "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 845                ])
 846
 847                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 848                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 849                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 850                        iJSON["aciValue"]["currency"]
 851                    )))
 852
 853            if "currentPrice" in iJSON.keys():
 854                info.append(splitLine)
 855
 856                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 857                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 858
 859                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 860                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 861                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 862                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 863                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 864
 865                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 866                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 867
 868                info.extend([
 869                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 870                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 871                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 872                    )),
 873                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 874                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 875                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 876                    )),
 877                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 878                        "{:.2f}%{}".format(
 879                            iJSON["currentPrice"]["changes"],
 880                            " ({}{:.2f} {})".format(
 881                                "+" if bondChangesDelta > 0 else "",
 882                                bondChangesDelta,
 883                                aciCurrency
 884                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 885                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 886                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 887                                currency
 888                            ),
 889                        )
 890                    ),
 891                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 892                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 893                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 894                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 895                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 896                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 897                    )),
 898                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 899                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 900                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 901                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 902                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 903                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 904                    )),
 905                ])
 906
 907            if "lot" in iJSON.keys():
 908                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 909
 910            if "step" in iJSON.keys() and iJSON["step"] != 0:
 911                info.append("| Minimum price increment (step):                             | {:<54} |\n".format(iJSON["step"]))
 912
 913            # Add bond payment calendar:
 914            if iJSON["type"] == "Bonds":
 915                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 916                info.extend(["\n", strCalendar])
 917
 918            infoText += "".join(info)
 919
 920            if show:
 921                uLogger.info("{}".format(infoText))
 922
 923            else:
 924                uLogger.debug("{}".format(infoText))
 925
 926            if self.infoFile is not None:
 927                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 928                    fH.write(infoText)
 929
 930                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 931
 932        return infoText
 933
 934    def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
 935        """
 936        Search and return raw broker's information about instrument by its ticker.
 937        `ticker` must be defined! If debug=True then print all debug messages.
 938
 939        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 940        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 941        :param debug: if `True` then print all debug console messages.
 942        :return: JSON formatted data with information about instrument.
 943        """
 944        tickerJSON = {}
 945        if debug:
 946            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 947
 948        if not self.ticker:
 949            uLogger.warning("self.ticker variable is not be empty!")
 950
 951        else:
 952            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 953                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 954                raise Exception("Instrument not allowed")
 955
 956            if not self.iList:
 957                self.iList = self.Listing()
 958
 959            if self.ticker in self.iList["Shares"].keys():
 960                tickerJSON = self.iList["Shares"][self.ticker]
 961                if debug:
 962                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 963
 964            elif self.ticker in self.iList["Currencies"].keys():
 965                tickerJSON = self.iList["Currencies"][self.ticker]
 966                if debug:
 967                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 968
 969            elif self.ticker in self.iList["Bonds"].keys():
 970                tickerJSON = self.iList["Bonds"][self.ticker]
 971                if debug:
 972                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 973
 974            elif self.ticker in self.iList["Etfs"].keys():
 975                tickerJSON = self.iList["Etfs"][self.ticker]
 976                if debug:
 977                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 978
 979            elif self.ticker in self.iList["Futures"].keys():
 980                tickerJSON = self.iList["Futures"][self.ticker]
 981                if debug:
 982                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 983
 984        if tickerJSON:
 985            self.figi = tickerJSON["figi"]
 986
 987            if requestPrice:
 988                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 989
 990                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 991                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 992
 993                else:
 994                    tickerJSON["currentPrice"]["changes"] = 0
 995
 996            if show:
 997                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 998
 999        else:
1000            if show:
1001                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
1002
1003        return tickerJSON
1004
1005    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
1006        """
1007        Search and return raw broker's information about instrument by its FIGI.
1008        `figi` must be defined! If debug=True then print all debug messages.
1009
1010        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1011        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1012        :param debug: if `True` then print all debug console messages.
1013        :return: JSON formatted data with information about instrument.
1014        """
1015        figiJSON = {}
1016        if debug:
1017            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
1018
1019        if not self.figi:
1020            uLogger.warning("self.figi variable is not be empty!")
1021
1022        else:
1023            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1024                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
1025                raise Exception("Instrument not allowed")
1026
1027            if not self.iList:
1028                self.iList = self.Listing()
1029
1030            for item in self.iList["Shares"].keys():
1031                if self.figi == self.iList["Shares"][item]["figi"]:
1032                    figiJSON = self.iList["Shares"][item]
1033
1034                    if debug:
1035                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
1036
1037                    break
1038
1039            if not figiJSON:
1040                for item in self.iList["Currencies"].keys():
1041                    if self.figi == self.iList["Currencies"][item]["figi"]:
1042                        figiJSON = self.iList["Currencies"][item]
1043
1044                        if debug:
1045                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1046
1047                        break
1048
1049            if not figiJSON:
1050                for item in self.iList["Bonds"].keys():
1051                    if self.figi == self.iList["Bonds"][item]["figi"]:
1052                        figiJSON = self.iList["Bonds"][item]
1053
1054                        if debug:
1055                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1056
1057                        break
1058
1059            if not figiJSON:
1060                for item in self.iList["Etfs"].keys():
1061                    if self.figi == self.iList["Etfs"][item]["figi"]:
1062                        figiJSON = self.iList["Etfs"][item]
1063
1064                        if debug:
1065                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1066
1067                        break
1068
1069            if not figiJSON:
1070                for item in self.iList["Futures"].keys():
1071                    if self.figi == self.iList["Futures"][item]["figi"]:
1072                        figiJSON = self.iList["Futures"][item]
1073
1074                        if debug:
1075                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1076
1077                        break
1078
1079        if figiJSON:
1080            self.figi = figiJSON["figi"]
1081            self.ticker = figiJSON["ticker"]
1082
1083            if requestPrice:
1084                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1085
1086                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1087                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1088
1089                else:
1090                    figiJSON["currentPrice"]["changes"] = 0
1091
1092            if show:
1093                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1094
1095        else:
1096            if show:
1097                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1098
1099        return figiJSON
1100
1101    def GetCurrentPrices(self, show: bool = True) -> dict:
1102        """
1103        Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record:
1104        `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1105
1106        See also:
1107
1108        :param show: if `True` then print DOM to log and console.
1109        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1110        """
1111        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1112
1113        if self.depth < 1:
1114            uLogger.error("Depth of Market (DOM) must be >=1!")
1115            raise Exception("Incorrect value")
1116
1117        if not (self.ticker or self.figi):
1118            uLogger.error("self.ticker or self.figi variables must be defined!")
1119            raise Exception("Ticker or FIGI required")
1120
1121        if self.ticker and not self.figi:
1122            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1123            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1124
1125        if not self.ticker and self.figi:
1126            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1127            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1128
1129        if not self.figi:
1130            uLogger.error("FIGI is not defined!")
1131            raise Exception("Ticker or FIGI required")
1132
1133        else:
1134            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1135
1136            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1137            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1138            self.body = str({"figi": self.figi, "depth": self.depth})
1139            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")
1140
1141            if pricesResponse:
1142                # list of dicts with sellers orders:
1143                prices["buy"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1144
1145                # list of dicts with buyers orders:
1146                prices["sell"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1147
1148                # max price of instrument at this time:
1149                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1150
1151                # min price of instrument at this time:
1152                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1153
1154                # last price of deal with instrument:
1155                prices["lastPrice"] = NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]) if "lastPrice" in pricesResponse.keys() else 0
1156
1157                # last close price of instrument:
1158                prices["closePrice"] = NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]) if "closePrice" in pricesResponse.keys() else 0
1159
1160            else:
1161                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1162                uLogger.debug("Server response: {}".format(pricesResponse))
1163
1164            if show:
1165                if prices["buy"] or prices["sell"]:
1166                    info = [
1167                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1168                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1169                            self.ticker,
1170                            self.figi,
1171                            self.depth,
1172                        ),
1173                        uLog.sepShort, "\n",
1174                        " Orders of Buyers   | Orders of Sellers\n",
1175                        uLog.sepShort, "\n",
1176                        " Sell prices (vol.) | Buy prices (vol.)\n",
1177                        uLog.sepShort, "\n",
1178                    ]
1179
1180                    if not prices["buy"]:
1181                        info.append("                    | No orders!\n")
1182                        sumBuy = 0
1183
1184                    else:
1185                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1186                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1187                        for item in maxMinSorted:
1188                            info.append("                    | {} ({})\n".format(item["price"], item["quantity"]))
1189
1190                    if not prices["sell"]:
1191                        info.append("No orders!          |\n")
1192                        sumSell = 0
1193
1194                    else:
1195                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1196                        for item in prices["sell"]:
1197                            info.append("{:>19} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1198
1199                    info.extend([
1200                        uLog.sepShort, "\n",
1201                        "{:>19} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1202                        uLog.sepShort, "\n",
1203                    ])
1204
1205                    infoText = "".join(info)
1206
1207                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1208
1209                else:
1210                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1211
1212        return prices
1213
1214    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1215        """
1216        This method get and show information about all available broker instruments for current user account.
1217        If `instrumentsFile` string is not empty then also save information to this file.
1218
1219        :param show: if `True` then print results to console, if `False` - print only to file.
1220        :return: multi-lines string with all available broker instruments
1221        """
1222        if not self.iList:
1223            self.iList = self.Listing()
1224
1225        info = [
1226            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1227            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1228        ]
1229
1230        # add instruments count by type:
1231        for iType in self.iList.keys():
1232            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1233
1234        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1235        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1236
1237        # generating info tables with all instruments by type:
1238        for iType in self.iList.keys():
1239            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1240
1241            for instrument in self.iList[iType].keys():
1242                iName = self.iList[iType][instrument]["name"]  # instrument's name
1243                if len(iName) > 57:
1244                    iName = "{}...".format(iName[:54])  # right trim for a long string
1245
1246                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1247                    self.iList[iType][instrument]["ticker"],
1248                    iName,
1249                    self.iList[iType][instrument]["figi"],
1250                    self.iList[iType][instrument]["currency"],
1251                    self.iList[iType][instrument]["lot"],
1252                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1253                ))
1254
1255        infoText = "".join(info)
1256
1257        if show:
1258            uLogger.info(infoText)
1259
1260        if self.instrumentsFile:
1261            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1262                fH.write(infoText)
1263
1264            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1265
1266        return infoText
1267
1268    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1269        """
1270        This method search and show information about instruments by part of its ticker, FIGI or name.
1271        If `searchResultsFile` string is not empty then also save information to this file.
1272
1273        :param pattern: string with part of ticker, FIGI or instrument's name.
1274        :param show: if `True` then print results to console, if `False` - return list of result only.
1275        :return: list of dictionaries with all found instruments.
1276        """
1277        if not self.iList:
1278            self.iList = self.Listing()
1279
1280        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1281        compiledPattern = re.compile(pattern, re.IGNORECASE)
1282
1283        for iType in self.iList:
1284            for instrument in self.iList[iType].values():
1285                searchResult = compiledPattern.search(" ".join(
1286                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1287                ))
1288
1289                if searchResult:
1290                    searchResults[iType][instrument["ticker"]] = instrument
1291
1292        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1293        info = [
1294            "# Search results\n\n",
1295            "* **Search pattern:** [{}]\n".format(pattern),
1296            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1297            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1298        ]
1299        infoShort = info[:]
1300
1301        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1302        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1303        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1304
1305        if resultsLen == 0:
1306            info.append("\nNo results\n")
1307            infoShort.append("\nNo results\n")
1308            uLogger.warning("No results. Try changing your search pattern.")
1309
1310        else:
1311            for iType in searchResults:
1312                iTypeValuesCount = len(searchResults[iType].values())
1313                if iTypeValuesCount > 0:
1314                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1315                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1316
1317                    for instrument in searchResults[iType].values():
1318                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1319                            instrument["type"],
1320                            instrument["ticker"],
1321                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1322                            instrument["figi"],
1323                        ))
1324
1325                    if iTypeValuesCount <= 5:
1326                        infoShort.extend(info[-iTypeValuesCount:])
1327
1328                    else:
1329                        infoShort.extend(info[-5:])
1330                        infoShort.append(skippedLine)
1331
1332        infoText = "".join(info)
1333        infoTextShort = "".join(infoShort)
1334
1335        if show:
1336            uLogger.info(infoTextShort)
1337            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1338
1339        if self.searchResultsFile:
1340            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1341                fH.write(infoText)
1342
1343            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1344
1345        return searchResults
1346
1347    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1348        """
1349        Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
1350
1351        :param instruments: list of strings with tickers or FIGIs.
1352        :return: list with unique instrument FIGIs only.
1353        """
1354        requestedInstruments = []
1355        for iName in instruments:
1356            if iName not in self.aliases.keys():
1357                if iName not in requestedInstruments:
1358                    requestedInstruments.append(iName)
1359
1360            else:
1361                if iName not in requestedInstruments:
1362                    if self.aliases[iName] not in requestedInstruments:
1363                        requestedInstruments.append(self.aliases[iName])
1364
1365        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1366
1367        onlyUniqueFIGIs = []
1368        for iName in requestedInstruments:
1369            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1370                continue
1371
1372            self.ticker = iName
1373            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1374
1375            if not iData:
1376                self.ticker = ""
1377                self.figi = iName
1378
1379                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1380
1381                if not iData:
1382                    self.figi = ""
1383                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1384
1385            if iData and iData["figi"] not in onlyUniqueFIGIs:
1386                onlyUniqueFIGIs.append(iData["figi"])
1387
1388        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1389
1390        return onlyUniqueFIGIs
1391
1392    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1393        """
1394        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1395        See limits: https://tinkoff.github.io/investAPI/limits/
1396        If `pricesFile` string is not empty then also save information to this file.
1397
1398        :param instruments: list of strings with tickers or FIGIs.
1399        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1400        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1401                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1402        """
1403        if instruments is None or not instruments:
1404            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1405            raise Exception("Ticker or FIGI required")
1406
1407        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1408
1409        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1410
1411        iList = []  # trying to get info and current prices about all unique instruments:
1412        for self.figi in onlyUniqueFIGIs:
1413            iData = self.SearchByFIGI(requestPrice=True)
1414            iList.append(iData)
1415
1416        self.ShowListOfPrices(iList, show)
1417
1418        return iList
1419
1420    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1421        """
1422        Show table contains current prices of given instruments.
1423
1424        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1425                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1426        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1427        :return: multilines text in Markdown format as a table contains current prices.
1428        """
1429        infoText = ""
1430
1431        if show or self.pricesFile:
1432            info = [
1433                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1434                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1435                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1436            ]
1437
1438            for item in iList:
1439                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1440                    item["ticker"],
1441                    item["figi"],
1442                    item["type"],
1443                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1444                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1445                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1446                    "{} / {}".format(
1447                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1448                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1449                    ),
1450                    "{} / {}".format(
1451                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1452                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1453                    ),
1454                    item["currency"],
1455                ))
1456
1457            infoText = "".join(info)
1458
1459            if show:
1460                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1461
1462            if self.pricesFile:
1463                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1464                    fH.write(infoText)
1465
1466                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1467
1468        return infoText
1469
1470    def RequestTradingStatus(self) -> dict:
1471        """
1472        Requesting trading status for the instrument defined by `figi` variable.
1473        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1474        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1475
1476        :return: dictionary with trading status attributes. Response example:
1477                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1478                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1479        """
1480        if self.figi is None or not self.figi:
1481            uLogger.error("Variable `figi` must be defined for using this method!")
1482            raise Exception("FIGI required")
1483
1484        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1485
1486        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1487        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1488        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1489
1490        uLogger.debug("Records about current trading status successfully received")
1491
1492        return tradingStatus
1493
1494    def RequestPortfolio(self) -> dict:
1495        """
1496        Requesting actual user's portfolio for current `accountId`.
1497        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1498        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1499
1500        :return: dictionary with user's portfolio.
1501        """
1502        if self.accountId is None or not self.accountId:
1503            uLogger.error("Variable `accountId` must be defined for using this method!")
1504            raise Exception("Account ID required")
1505
1506        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1507
1508        self.body = str({"accountId": self.accountId})
1509        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1510        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1511
1512        uLogger.debug("Records about user's portfolio successfully received")
1513
1514        return rawPortfolio
1515
1516    def RequestPositions(self) -> dict:
1517        """
1518        Requesting open positions by currencies and instruments for current `accountId`.
1519        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1520        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1521
1522        :return: dictionary with open positions by instruments.
1523        """
1524        if self.accountId is None or not self.accountId:
1525            uLogger.error("Variable `accountId` must be defined for using this method!")
1526            raise Exception("Account ID required")
1527
1528        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1529
1530        self.body = str({"accountId": self.accountId})
1531        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1532        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1533
1534        uLogger.debug("Records about current open positions successfully received")
1535
1536        return rawPositions
1537
1538    def RequestPendingOrders(self) -> list:
1539        """
1540        Requesting current actual pending orders for current `accountId`.
1541        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1542        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1543
1544        :return: list of dictionaries with pending orders.
1545        """
1546        if self.accountId is None or not self.accountId:
1547            uLogger.error("Variable `accountId` must be defined for using this method!")
1548            raise Exception("Account ID required")
1549
1550        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1551
1552        self.body = str({"accountId": self.accountId})
1553        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1554        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1555
1556        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1557
1558        return rawOrders
1559
1560    def RequestStopOrders(self) -> list:
1561        """
1562        Requesting current actual stop orders for current `accountId`.
1563        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1564        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1565
1566        :return: list of dictionaries with stop orders.
1567        """
1568        if self.accountId is None or not self.accountId:
1569            uLogger.error("Variable `accountId` must be defined for using this method!")
1570            raise Exception("Account ID required")
1571
1572        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1573
1574        self.body = str({"accountId": self.accountId})
1575        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1576        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1577
1578        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1579
1580        return rawStopOrders
1581
1582    def Overview(self, show: bool = False, details: str = "full") -> dict:
1583        """
1584        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1585        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1586        are defined then also save information to file.
1587
1588        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1589        many requests about the state of the portfolio, and then, based on the received data, a large number
1590        of calculation and statistics are collected.
1591
1592        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1593        :param details: how detailed should the information be? You should specify one of strings:
1594                        `full` - shows full available information about portfolio status (by default),
1595                        `positions` - shows only open positions,
1596                        `digest` - show a short digest of the portfolio status,
1597                        `analytics` - shows only the analytics section and the distribution of the portfolio by various categories,
1598                        `orders` - shows only sections of open limits and stop orders.
1599        :return: dictionary with client's raw portfolio and some statistics.
1600        """
1601        if self.accountId is None or not self.accountId:
1602            uLogger.error("Variable `accountId` must be defined for using this method!")
1603            raise Exception("Account ID required")
1604
1605        view = {
1606            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1607                "headers": {},  # list of dictionaries, response headers without "positions" section
1608                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1609                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1610                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1611                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1612                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1613                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1614                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1615                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1616                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1617            },
1618            "stat": {  # --- some statistics calculated using "raw" sections:
1619                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1620                "availableRUB": 0.,  # available rubles (without other currencies)
1621                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1622                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1623                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1624                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1625                "sharesCostRUB": 0.,  # costs of all shares in RUB
1626                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1627                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1628                "futuresCostRUB": 0.,  # costs of all futures in RUB
1629                "Currencies": [],  # list of dictionaries of all currencies statistics
1630                "Shares": [],  # list of dictionaries of all shares statistics
1631                "Bonds": [],  # list of dictionaries of all bonds statistics
1632                "Etfs": [],  # list of dictionaries of all etfs statistics
1633                "Futures": [],  # list of dictionaries of all futures statistics
1634                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1635                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1636                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1637                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1638                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1639            },
1640            "analytics": {  # --- some analytics of portfolio:
1641                "distrByAssets": {},  # portfolio distribution by assets
1642                "distrByCompanies": {},  # portfolio distribution by companies
1643                "distrBySectors": {},  # portfolio distribution by sectors
1644                "distrByCurrencies": {},  # portfolio distribution by currencies
1645                "distrByCountries": {},  # portfolio distribution by countries
1646            }
1647        }
1648
1649        details = details.lower()
1650        availableDetails = ["full", "positions", "digest", "analytics", "orders"]
1651        if details not in availableDetails:
1652            details = "full"
1653            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1654
1655        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1656
1657        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1658        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1659        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1660        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1661
1662        # save response headers without "positions" section:
1663        for key in portfolioResponse.keys():
1664            if key != "positions":
1665                view["raw"]["headers"][key] = portfolioResponse[key]
1666
1667            else:
1668                continue
1669
1670        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1671        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1672        for item in portfolioResponse["positions"]:
1673            if item["instrumentType"] == "currency":
1674                self.figi = item["figi"]
1675                curr = self.SearchByFIGI(requestPrice=False)
1676
1677                # current price of currency in RUB:
1678                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1679                    "name": curr["name"],
1680                    "currentPrice": NanoToFloat(
1681                        item["currentPrice"]["units"],
1682                        item["currentPrice"]["nano"]
1683                    ),
1684                }
1685
1686                view["raw"]["Currencies"].append(item)
1687
1688            elif item["instrumentType"] == "share":
1689                view["raw"]["Shares"].append(item)
1690
1691            elif item["instrumentType"] == "bond":
1692                view["raw"]["Bonds"].append(item)
1693
1694            elif item["instrumentType"] == "etf":
1695                view["raw"]["Etfs"].append(item)
1696
1697            elif item["instrumentType"] == "futures":
1698                view["raw"]["Futures"].append(item)
1699
1700            else:
1701                continue
1702
1703        # how many volume of currencies (by ISO currency name) are blocked:
1704        for item in view["raw"]["positions"]["blocked"]:
1705            blocked = NanoToFloat(item["units"], item["nano"])
1706            if blocked > 0:
1707                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1708
1709        # how many volume of instruments (by FIGI) are blocked:
1710        for item in view["raw"]["positions"]["securities"]:
1711            blocked = int(item["blocked"])
1712            if blocked > 0:
1713                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1714
1715        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1716
1717        if "rub" in allBlocked.keys():
1718            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1719
1720        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1721        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1722        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1723        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1724        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1725        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1726        view["stat"]["portfolioCostRUB"] = sum([
1727            view["stat"]["allCurrenciesCostRUB"],
1728            view["stat"]["sharesCostRUB"],
1729            view["stat"]["bondsCostRUB"],
1730            view["stat"]["etfsCostRUB"],
1731            view["stat"]["futuresCostRUB"],
1732        ])
1733
1734        # --- calculating some portfolio statistics:
1735        byComp = {}  # distribution by companies
1736        bySect = {}  # distribution by sectors
1737        byCurr = {}  # distribution by currencies (include RUB)
1738        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1739        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1740
1741        for item in portfolioResponse["positions"]:
1742            self.figi = item["figi"]
1743            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1744
1745            if instrument:
1746                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1747                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1748
1749                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1750                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1751
1752                else:
1753                    blocked = 0
1754
1755                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1756                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1757                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1758                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1759                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1760                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1761                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1762                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1763                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1764                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1765                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1766                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1767
1768                statData = {
1769                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1770                    "ticker": instrument["ticker"],  # ticker by FIGI
1771                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1772                    "volume": volume,  # available volume of instrument
1773                    "lots": lots,  # volume in lots of instrument
1774                    "direction": direction,  # direction of an instrument's position: short or long
1775                    "blocked": blocked,  # blocked volume of currency or instrument
1776                    "currentPrice": curPrice,  # current instrument's price in basic asset
1777                    "average": average,  # current average position price
1778                    "cost": cost,  # current cost of all volume of instrument in basic asset
1779                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1780                    "costRUB": costRUB,  # cost of instrument in ruble
1781                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1782                    "profit": profit,  # expected profit at current moment
1783                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1784                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1785                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1786                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1787                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1788                    "step": instrument["step"],  # minimum price increment
1789                }
1790
1791                # adding distribution by unique countries:
1792                if statData["country"] not in byCountry.keys():
1793                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1794
1795                else:
1796                    byCountry[statData["country"]]["cost"] += costRUB
1797                    byCountry[statData["country"]]["percent"] += percentCostRUB
1798
1799                if item["instrumentType"] != "currency":
1800                    # adding distribution by unique companies:
1801                    if statData["name"]:
1802                        if statData["name"] not in byComp.keys():
1803                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1804
1805                        else:
1806                            byComp[statData["name"]]["cost"] += costRUB
1807                            byComp[statData["name"]]["percent"] += percentCostRUB
1808
1809                    # adding distribution by unique sectors:
1810                    if statData["sector"] not in bySect.keys():
1811                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1812
1813                    else:
1814                        bySect[statData["sector"]]["cost"] += costRUB
1815                        bySect[statData["sector"]]["percent"] += percentCostRUB
1816
1817                # adding distribution by unique currencies:
1818                if currency not in byCurr.keys():
1819                    byCurr[currency] = {
1820                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1821                        "cost": costRUB,
1822                        "percent": percentCostRUB
1823                    }
1824
1825                else:
1826                    byCurr[currency]["cost"] += costRUB
1827                    byCurr[currency]["percent"] += percentCostRUB
1828
1829                # saving statistics for every instrument:
1830                if item["instrumentType"] == "currency":
1831                    view["stat"]["Currencies"].append(statData)
1832
1833                    # update dict with free funds for trading (total - blocked) by currencies
1834                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1835                    view["stat"]["funds"][currency] = {
1836                        "total": volume,
1837                        "totalCostRUB": costRUB,  # total volume cost in rubles
1838                        "free": volume - blocked,
1839                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1840                    }
1841
1842                elif item["instrumentType"] == "share":
1843                    view["stat"]["Shares"].append(statData)
1844
1845                elif item["instrumentType"] == "bond":
1846                    view["stat"]["Bonds"].append(statData)
1847
1848                elif item["instrumentType"] == "etf":
1849                    view["stat"]["Etfs"].append(statData)
1850
1851                elif item["instrumentType"] == "Futures":
1852                    view["stat"]["Futures"].append(statData)
1853
1854                else:
1855                    continue
1856
1857        # total changes in Russian Ruble:
1858        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1859        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1860        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1861        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1862        view["stat"]["funds"]["rub"] = {
1863            "total": view["stat"]["availableRUB"],
1864            "totalCostRUB": view["stat"]["availableRUB"],
1865            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1866            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1867        }
1868
1869        # --- pending orders sector data:
1870        uniquePendingOrders = []
1871        uniquePendingOrdersFIGIs = []
1872        for item in view["raw"]["orders"]:
1873            if item["figi"] not in uniquePendingOrdersFIGIs:
1874                uniquePendingOrdersFIGIs.append(item["figi"])
1875                uniquePendingOrders.append(item)
1876
1877        for item in uniquePendingOrders:
1878            self.figi = item["figi"]
1879            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1880
1881            if instrument:
1882                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1883                orderType = TKS_ORDER_TYPES[item["orderType"]]
1884                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1885                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1886
1887                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1888                if item["direction"] == "ORDER_DIRECTION_BUY":
1889                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1890
1891                else:
1892                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1893
1894                # requested price for order execution:
1895                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1896
1897                # necessary changes in percent to reach target from current price:
1898                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1899
1900                view["stat"]["orders"].append({
1901                    "orderID": item["orderId"],  # orderId number parameter of current order
1902                    "figi": item["figi"],  # FIGI identification
1903                    "ticker": instrument["ticker"],  # ticker name by FIGI
1904                    "lotsRequested": item["lotsRequested"],  # requested lots value
1905                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1906                    "currentPrice": lastPrice,  # current instrument's price for defined action
1907                    "targetPrice": target,  # requested price for order execution in base currency
1908                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1909                    "percentChanges": changes,  # changes in percent to target from current price
1910                    "currency": item["currency"],  # instrument's currency name
1911                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1912                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1913                    "status": orderState,  # order status from TKS_ORDER_STATES
1914                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1915                })
1916
1917        # --- stop orders sector data:
1918        uniqueStopOrders = []
1919        uniqueStopOrdersFIGIs = []
1920        for item in view["raw"]["stopOrders"]:
1921            if item["figi"] not in uniqueStopOrdersFIGIs:
1922                uniqueStopOrdersFIGIs.append(item["figi"])
1923                uniqueStopOrders.append(item)
1924
1925        for item in uniqueStopOrders:
1926            self.figi = item["figi"]
1927            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1928
1929            if instrument:
1930                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1931                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1932                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1933
1934                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1935                if "expirationTime" in item.keys():
1936                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1937                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1938
1939                else:
1940                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1941                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1942
1943                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1944                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1945                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1946
1947                else:
1948                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1949
1950                # requested price when stop-order executed:
1951                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1952
1953                # price for limit-order, set up when stop-order executed:
1954                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1955
1956                # necessary changes in percent to reach target from current price:
1957                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1958
1959                view["stat"]["stopOrders"].append({
1960                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1961                    "figi": item["figi"],  # FIGI identification
1962                    "ticker": instrument["ticker"],  # ticker name by FIGI
1963                    "lotsRequested": item["lotsRequested"],  # requested lots value
1964                    "currentPrice": lastPrice,  # current instrument's price for defined action
1965                    "targetPrice": target,  # requested price for stop-order execution in base currency
1966                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1967                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1968                    "percentChanges": changes,  # changes in percent to target from current price
1969                    "currency": item["currency"],  # instrument's currency name
1970                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1971                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1972                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1973                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1974                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1975                })
1976
1977        # --- calculating data for analytics section:
1978        # portfolio distribution by assets:
1979        view["analytics"]["distrByAssets"] = {
1980            "Ruble": {
1981                "uniques": 1,
1982                "cost": view["stat"]["availableRUB"],
1983                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1984            },
1985            "Currencies": {
1986                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
1987                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
1988                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1989            },
1990            "Shares": {
1991                "uniques": len(view["stat"]["Shares"]),
1992                "cost": view["stat"]["sharesCostRUB"],
1993                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1994            },
1995            "Bonds": {
1996                "uniques": len(view["stat"]["Bonds"]),
1997                "cost": view["stat"]["bondsCostRUB"],
1998                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1999            },
2000            "Etfs": {
2001                "uniques": len(view["stat"]["Etfs"]),
2002                "cost": view["stat"]["etfsCostRUB"],
2003                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2004            },
2005            "Futures": {
2006                "uniques": len(view["stat"]["Futures"]),
2007                "cost": view["stat"]["futuresCostRUB"],
2008                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2009            },
2010        }
2011
2012        # portfolio distribution by companies:
2013        view["analytics"]["distrByCompanies"]["All money cash"] = {
2014            "ticker": "",
2015            "cost": view["stat"]["allCurrenciesCostRUB"],
2016            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2017        }
2018        view["analytics"]["distrByCompanies"].update(byComp)
2019
2020        # portfolio distribution by sectors:
2021        view["analytics"]["distrBySectors"]["All money cash"] = {
2022            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2023            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2024        }
2025        view["analytics"]["distrBySectors"].update(bySect)
2026
2027        # portfolio distribution by currencies:
2028        view["analytics"]["distrByCurrencies"].update(byCurr)
2029        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2030        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2031
2032        # portfolio distribution by countries:
2033        view["analytics"]["distrByCountries"].update(byCountry)
2034
2035        # --- Prepare text statistics overview in human-readable:
2036        if show:
2037            # Whatever the value `details`, header not changes:
2038            info = [
2039                "# Client's portfolio\n\n",
2040                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2041                "* **Account ID:** [{}]\n".format(self.accountId),
2042            ]
2043
2044            if details in ["full", "positions", "digest"]:
2045                info.extend([
2046                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2047                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2048                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2049                        view["stat"]["totalChangesRUB"],
2050                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2051                        view["stat"]["totalChangesPercentRUB"],
2052                    ),
2053                ])
2054
2055            if details in ["full", "positions"]:
2056                info.extend([
2057                    "## Open positions\n\n",
2058                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2059                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2060                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2061                        "{:.2f} ({:.2f}) rub".format(
2062                            view["stat"]["availableRUB"],
2063                            view["stat"]["blockedRUB"],
2064                        )
2065                    )
2066                ])
2067
2068                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2069                    return [
2070                        "|                             |                                 |          |              |              |                     |                              |\n",
2071                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2072                            noTradeStr if noTradeStr else typeStr,
2073                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2074                        ),
2075                    ]
2076
2077                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2078                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2079                        "{} [{}]".format(data["ticker"], data["figi"]),
2080                        "{:.2f} ({:.2f}) {}".format(
2081                            data["volume"],
2082                            data["blocked"],
2083                            data["currency"],
2084                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2085                            data["volume"],
2086                            data["blocked"],
2087                        ),
2088                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2089                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2090                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2091                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2092                        "{}{:.2f} {} ({}{:.2f}%)".format(
2093                            "+" if data["profit"] > 0 else "",
2094                            data["profit"], data["baseCurrencyName"],
2095                            "+" if data["percentProfit"] > 0 else "",
2096                            data["percentProfit"],
2097                        ),
2098                    )
2099
2100                # --- Show currencies section:
2101                if view["stat"]["Currencies"]:
2102                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2103                    for item in view["stat"]["Currencies"]:
2104                        info.append(_InfoStr(item, showCurrencyName=True))
2105
2106                else:
2107                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2108
2109                # --- Show shares section:
2110                if view["stat"]["Shares"]:
2111                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2112
2113                    for item in view["stat"]["Shares"]:
2114                        info.append(_InfoStr(item))
2115
2116                else:
2117                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2118
2119                # --- Show bonds section:
2120                if view["stat"]["Bonds"]:
2121                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2122
2123                    for item in view["stat"]["Bonds"]:
2124                        info.append(_InfoStr(item))
2125
2126                else:
2127                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2128
2129                # --- Show etfs section:
2130                if view["stat"]["Etfs"]:
2131                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2132
2133                    for item in view["stat"]["Etfs"]:
2134                        info.append(_InfoStr(item))
2135
2136                else:
2137                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2138
2139                # --- Show futures section:
2140                if view["stat"]["Futures"]:
2141                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2142
2143                    for item in view["stat"]["Futures"]:
2144                        info.append(_InfoStr(item))
2145
2146                else:
2147                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2148
2149            if details in ["full", "orders"]:
2150                # --- Show pending orders section:
2151                if view["stat"]["orders"]:
2152                    info.extend([
2153                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2154                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2155                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2156                    ])
2157
2158                    for item in view["stat"]["orders"]:
2159                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2160                            "{} [{}]".format(item["ticker"], item["figi"]),
2161                            item["orderID"],
2162                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2163                            "{} {} ({}{:.2f}%)".format(
2164                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2165                                item["baseCurrencyName"],
2166                                "+" if item["percentChanges"] > 0 else "",
2167                                float(item["percentChanges"]),
2168                            ),
2169                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2170                            item["action"],
2171                            item["type"],
2172                            item["date"],
2173                        ))
2174
2175                else:
2176                    info.append("\n## Total pending limit-orders: 0\n")
2177
2178                # --- Show stop orders section:
2179                if view["stat"]["stopOrders"]:
2180                    info.extend([
2181                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2182                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2183                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2184                    ])
2185
2186                    for item in view["stat"]["stopOrders"]:
2187                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2188                            "{} [{}]".format(item["ticker"], item["figi"]),
2189                            item["orderID"],
2190                            item["lotsRequested"],
2191                            "{} {} ({}{:.2f}%)".format(
2192                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2193                                item["baseCurrencyName"],
2194                                "+" if item["percentChanges"] > 0 else "",
2195                                float(item["percentChanges"]),
2196                            ),
2197                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2198                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2199                            item["action"],
2200                            item["type"],
2201                            item["expType"],
2202                            item["createDate"],
2203                            item["expDate"],
2204                        ))
2205
2206                else:
2207                    info.append("\n## Total stop-orders: 0\n")
2208
2209            if details in ["full", "analytics"]:
2210                # -- Show analytics section:
2211                if view["stat"]["portfolioCostRUB"] > 0:
2212                    info.extend([
2213                        "\n# Analytics\n"
2214                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2215                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2216                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2217                            view["stat"]["totalChangesRUB"],
2218                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2219                            view["stat"]["totalChangesPercentRUB"],
2220                        ),
2221                        "\n## Portfolio distribution by assets\n"
2222                        "\n| Type       | Uniques | Percent | Current cost       |\n",
2223                        "|------------|---------|---------|--------------------|\n",
2224                    ])
2225
2226                    for key in view["analytics"]["distrByAssets"].keys():
2227                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2228                            info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format(
2229                                key,
2230                                view["analytics"]["distrByAssets"][key]["uniques"],
2231                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2232                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2233                            ))
2234
2235                    maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()])
2236                    info.extend([
2237                        "\n## Portfolio distribution by companies\n"
2238                        "\n| Company{} | Percent | Current cost       |\n".format(" " * (maxLenNames - 7)),
2239                        "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)),
2240                    ])
2241
2242                    for company in view["analytics"]["distrByCompanies"].keys():
2243                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2244                            nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"])
2245                            info.append("| {} | {:<7} | {:<18} |\n".format(
2246                                "{}{}{}".format(
2247                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2248                                    company,
2249                                    "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)),
2250                                ),
2251                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2252                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2253                            ))
2254
2255                    maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()])
2256                    info.extend([
2257                        "\n## Portfolio distribution by sectors\n"
2258                        "\n| Sector{} | Percent | Current cost       |\n".format(" " * (maxLenSectors - 6)),
2259                        "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)),
2260                    ])
2261
2262                    for sector in view["analytics"]["distrBySectors"].keys():
2263                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2264                            info.append("| {}{} | {:<7} | {:<18} |\n".format(
2265                                sector,
2266                                "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)),
2267                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2268                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2269                            ))
2270
2271                    maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()])
2272                    info.extend([
2273                        "\n## Portfolio distribution by currencies\n"
2274                        "\n| Instruments currencies{} | Percent | Current cost       |\n".format(" " * (maxLenMoney - 22)),
2275                        "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)),
2276                    ])
2277
2278                    for curr in view["analytics"]["distrByCurrencies"].keys():
2279                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2280                            nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"])
2281                            info.append("| {} | {:<7} | {:<18} |\n".format(
2282                                "[{}] {}{}".format(
2283                                    curr,
2284                                    view["analytics"]["distrByCurrencies"][curr]["name"],
2285                                    "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen),
2286                                ),
2287                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2288                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2289                            ))
2290
2291                    maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()]))
2292                    info.extend([
2293                        "\n## Portfolio distribution by countries\n"
2294                        "\n| Assets by country{} | Percent | Current cost       |\n".format(" " * (maxLenCountry - 17)),
2295                        "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)),
2296                    ])
2297
2298                    for country in view["analytics"]["distrByCountries"].keys():
2299                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2300                            nameLen = len(country)
2301                            info.append("| {} | {:<7} | {:<18} |\n".format(
2302                                "{}{}".format(
2303                                    country,
2304                                    "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen),
2305                                ),
2306                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2307                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2308                            ))
2309
2310            infoText = "".join(info)
2311
2312            uLogger.info(infoText)
2313
2314            if details == "full" and self.overviewFile:
2315                filename = self.overviewFile
2316
2317            elif details == "digest" and self.overviewDigestFile:
2318                filename = self.overviewDigestFile
2319
2320            elif details == "positions" and self.overviewPositionsFile:
2321                filename = self.overviewPositionsFile
2322
2323            elif details == "orders" and self.overviewOrdersFile:
2324                filename = self.overviewOrdersFile
2325
2326            elif details == "analytics" and self.overviewAnalyticsFile:
2327                filename = self.overviewAnalyticsFile
2328
2329            else:
2330                filename = ""
2331
2332            if filename:
2333                with open(filename, "w", encoding="UTF-8") as fH:
2334                    fH.write(infoText)
2335
2336                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2337
2338        return view
2339
2340    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple:
2341        """
2342        Returns history operations between two given dates for current `accountId`.
2343        If `reportFile` string is not empty then also save human-readable report.
2344        Shows some statistical data of closed positions.
2345
2346        :param start: see docstring in `GetDatesAsString()` method
2347        :param end: see docstring in `GetDatesAsString()` method
2348        :param show: if `True` then also prints all records to the console.
2349        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2350        :return: original list of dictionaries with history of deals records from API ("operations" key):
2351                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2352                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2353        """
2354        if self.accountId is None or not self.accountId:
2355            uLogger.error("Variable `accountId` must be defined for using this method!")
2356            raise Exception("Account ID required")
2357
2358        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2359
2360        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2361
2362        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2363        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2364        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2365        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2366        customStat = {}  # custom statistics in additional to responseJSON
2367
2368        # --- output report in human-readable format:
2369        if show or self.reportFile:
2370            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2371            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2372            nextDay = ""
2373
2374            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2375
2376            if len(ops) > 0:
2377                customStat = {
2378                    "opsCount": 0,  # total operations count
2379                    "buyCount": 0,  # buy operations
2380                    "sellCount": 0,  # sell operations
2381                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2382                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2383                    "payIn": {"rub": 0.},  # Deposit brokerage account
2384                    "payOut": {"rub": 0.},  # Withdrawals
2385                    "divs": {"rub": 0.},  # Dividends income
2386                    "coupons": {"rub": 0.},  # Coupon's income
2387                    "brokerCom": {"rub": 0.},  # Service commissions
2388                    "serviceCom": {"rub": 0.},  # Service commissions
2389                    "marginCom": {"rub": 0.},  # Margin commissions
2390                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2391                }
2392
2393                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2394                for item in ops:
2395                    if item["state"] == "OPERATION_STATE_EXECUTED":
2396                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2397
2398                        # count buy operations:
2399                        if "_BUY" in item["operationType"]:
2400                            customStat["buyCount"] += 1
2401
2402                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2403                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2404
2405                            else:
2406                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2407
2408                        # count sell operations:
2409                        elif "_SELL" in item["operationType"]:
2410                            customStat["sellCount"] += 1
2411
2412                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2413                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2414
2415                            else:
2416                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2417
2418                        # count incoming operations:
2419                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2420                            if item["payment"]["currency"] in customStat["payIn"].keys():
2421                                customStat["payIn"][item["payment"]["currency"]] += payment
2422
2423                            else:
2424                                customStat["payIn"][item["payment"]["currency"]] = payment
2425
2426                        # count withdrawals operations:
2427                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2428                            if item["payment"]["currency"] in customStat["payOut"].keys():
2429                                customStat["payOut"][item["payment"]["currency"]] += payment
2430
2431                            else:
2432                                customStat["payOut"][item["payment"]["currency"]] = payment
2433
2434                        # count dividends income:
2435                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2436                            if item["payment"]["currency"] in customStat["divs"].keys():
2437                                customStat["divs"][item["payment"]["currency"]] += payment
2438
2439                            else:
2440                                customStat["divs"][item["payment"]["currency"]] = payment
2441
2442                        # count coupon's income:
2443                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2444                            if item["payment"]["currency"] in customStat["coupons"].keys():
2445                                customStat["coupons"][item["payment"]["currency"]] += payment
2446
2447                            else:
2448                                customStat["coupons"][item["payment"]["currency"]] = payment
2449
2450                        # count broker commissions:
2451                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2452                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2453                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2454
2455                            else:
2456                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2457
2458                        # count service commissions:
2459                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2460                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2461                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2462
2463                            else:
2464                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2465
2466                        # count margin commissions:
2467                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2468                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2469                                customStat["marginCom"][item["payment"]["currency"]] += payment
2470
2471                            else:
2472                                customStat["marginCom"][item["payment"]["currency"]] = payment
2473
2474                        # count withholding taxes:
2475                        elif "_TAX" in item["operationType"]:
2476                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2477                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2478
2479                            else:
2480                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2481
2482                        else:
2483                            continue
2484
2485                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2486
2487                # --- view "Actions" lines:
2488                info.extend([
2489                    "| 1                          | 2                             | 3                            | 4                    | 5                      |\n",
2490                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2491                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2492                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2493                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2494                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2495                    ),
2496                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2497                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2498                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2499                    ),
2500                ])
2501
2502                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2503                for key in opsKeys:
2504                    if key == "rub":
2505                        continue
2506
2507                    info.extend([
2508                        "|                            |                               | {:<28} |                      |                        |\n".format(
2509                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2510                        ),
2511                        "|                            |                               | {:<28} |                      |                        |\n".format(
2512                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2513                        ),
2514                    ])
2515
2516                info.append(splitLine1)
2517
2518                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2519                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2520                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2521                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2522                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2523                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2524                    )
2525
2526                # --- view "Payments" lines:
2527                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2528                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2529
2530                for key in paymentsKeys:
2531                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2532
2533                info.append(splitLine1)
2534
2535                # --- view "Commissions and taxes" lines:
2536                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2537                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2538
2539                for key in comKeys:
2540                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2541
2542                info.append(splitLine1)
2543
2544                info.extend([
2545                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2546                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2547                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2548                ])
2549
2550            else:
2551                info.append("Broker returned no operations during this period\n")
2552
2553            # --- view "Operations" section:
2554            for item in ops:
2555                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2556                    continue
2557
2558                else:
2559                    self.figi = item["figi"] if item["figi"] else ""
2560                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2561                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2562
2563                    # group of deals during one day:
2564                    if nextDay and item["date"].split("T")[0] != nextDay:
2565                        info.append(splitLine2)
2566                        nextDay = ""
2567
2568                    else:
2569                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2570
2571                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2572                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2573                        self.figi if self.figi else "—",
2574                        instrument["ticker"] if instrument else "—",
2575                        instrument["type"] if instrument else "—",
2576                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2577                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2578                        TKS_OPERATION_STATES[item["state"]],
2579                        TKS_OPERATION_TYPES[item["operationType"]],
2580                    ))
2581
2582            infoText = "".join(info)
2583
2584            if show:
2585                uLogger.info(infoText)
2586
2587            if self.reportFile:
2588                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2589                    fH.write(infoText)
2590
2591                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2592
2593        return ops, customStat
2594
2595    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2596        """
2597        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2598
2599        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2600        Warning! Broker server used ISO UTC time by default.
2601
2602        If `historyFile` is not `None` then method save history to file, otherwise return only pandas dataframe.
2603        Also, `historyFile` used to update history with `onlyMissing` parameter.
2604
2605        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2606
2607        :param start: see docstring in `GetDatesAsString()` method.
2608        :param end: see docstring in `GetDatesAsString()` method.
2609        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2610                         `"hour"`, `"day"`. Default: `"hour"`.
2611        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2612                            False by default. Warning! History appends only from last candle to current time
2613                            with always update last candle!
2614        :param csvSep: separator if csv-file is used, `,` by default.
2615        :param show: if `True` then also prints pandas dataframe to the console.
2616        :return: pandas dataframe with prices history. Headers of columns are defined by default:
2617                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2618        """
2619        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2620        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2621        history = None  # empty pandas object for history
2622
2623        if interval not in TKS_CANDLE_INTERVALS.keys():
2624            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2625            raise Exception("Incorrect value")
2626
2627        if not (self.ticker or self.figi):
2628            uLogger.error("Ticker or FIGI must be defined!")
2629            raise Exception("Ticker or FIGI required")
2630
2631        if self.ticker and not self.figi:
2632            instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False)
2633            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2634
2635        if self.figi and not self.ticker:
2636            instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False)
2637            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2638
2639        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2640        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2641        if interval.lower() != "day":
2642            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2643
2644        delta = dtEnd - dtStart  # current UTC time minus last time in file
2645        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2646
2647        # calculate history length in candles:
2648        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2649        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2650            length += 1  # to avoid fraction time
2651
2652        # calculate data blocks count:
2653        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2654
2655        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2656        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2657        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2658        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2659        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2660
2661        tempOld = None  # pandas object for old history, if --only-missing key present
2662        lastTime = None  # datetime object of last old candle in file
2663
2664        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2665            uLogger.debug("--only-missing key present, add only last missing candles...")
2666            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2667
2668            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2669
2670            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2671            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2672            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2673            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2674
2675            # get last datetime object from last string in file or minus 1 delta if file is empty:
2676            if len(tempOld) > 0:
2677                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2678
2679            else:
2680                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2681
2682            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2683
2684        responseJSONs = []  # raw history blocks of data
2685
2686        blockEnd = dtEnd
2687        for item in range(blocks):
2688            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2689            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2690
2691            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2692                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2693            ))
2694
2695            if blockStart == blockEnd:
2696                uLogger.debug("Skipped this zero-length block...")
2697
2698            else:
2699                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2700                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2701                self.body = str({
2702                    "figi": self.figi,
2703                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2704                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2705                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2706                })
2707                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False)
2708
2709                if "code" in responseJSON.keys():
2710                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2711
2712                else:
2713                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2714                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2715
2716                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2717
2718            blockEnd = blockStart
2719
2720        printCount = len(responseJSONs)  # candles to show in console
2721        if responseJSONs:
2722            tempHistory = pd.DataFrame(
2723                data={
2724                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2725                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2726                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2727                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2728                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2729                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2730                    "volume": [int(item["volume"]) for item in responseJSONs],
2731                },
2732                index=range(len(responseJSONs)),
2733                columns=["date", "time", "open", "high", "low", "close", "volume"],
2734            )
2735            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2736            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2737
2738            # append only newest candles to old history if --only-missing key present:
2739            if onlyMissing and tempOld is not None and lastTime is not None:
2740                index = 0  # find start index in tempHistory data:
2741
2742                for i, item in tempHistory.iterrows():
2743                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2744
2745                    if curTime == lastTime:
2746                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2747                        index = i
2748                        printCount = index + 1
2749                        break
2750
2751                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2752
2753            else:
2754                history = tempHistory  # if no `--only-missing` key then load full data from server
2755
2756            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2757
2758        if history is not None and not history.empty:
2759            if show:
2760                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2761                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2762                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2763                ))
2764
2765        else:
2766            uLogger.warning("Received an empty candles history!")
2767
2768        if self.historyFile is not None:
2769            if history is not None and not history.empty:
2770                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2771                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2772
2773            else:
2774                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2775
2776        else:
2777            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only pandas dataframe returns.")
2778
2779        return history
2780
2781    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2782        """
2783        Load candles history from csv-file and return pandas dataframe object.
2784
2785        See also: `History()` and `ShowHistoryChart()` methods.
2786
2787        :param filePath: path to csv-file to open.
2788        """
2789        loadedHistory = None  # init candles data object
2790
2791        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2792
2793        if os.path.exists(filePath):
2794            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as pandas dataframe
2795
2796            tfStr = self.priceModel.FormattedDelta(
2797                self.priceModel.timeframe,
2798                "{days} days {hours}h {minutes}m {seconds}s",
2799            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2800                self.priceModel.timeframe,
2801                "{hours}h {minutes}m {seconds}s",
2802            )
2803
2804            if loadedHistory is not None and not loadedHistory.empty:
2805                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2806                    len(loadedHistory),
2807                    tfStr,
2808                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2809                )
2810
2811            else:
2812                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2813
2814        else:
2815            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2816
2817        return loadedHistory
2818
2819    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2820        """
2821        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2822
2823        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2824        Default: `index.html` (both for interact and non-interact candlesticks chart).
2825
2826        See also: `History()` and `LoadHistory()` methods.
2827
2828        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2829        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2830                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2831                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2832                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2833        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2834                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2835        """
2836        if isinstance(candles, str):
2837            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2838            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2839
2840        elif isinstance(candles, pd.DataFrame):
2841            self.priceModel.prices = candles  # set candles chain from variable
2842            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2843
2844            if "datetime" not in candles.columns:
2845                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2846
2847        else:
2848            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2849            raise Exception("Incorrect value")
2850
2851        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2852
2853        if interact:
2854            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2855
2856            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2857
2858        else:
2859            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2860
2861            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2862
2863        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2864
2865    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2866        """
2867        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2868        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2869
2870        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2871
2872        :param operation: string "Buy" or "Sell".
2873        :param lots: volume, integer count of lots >= 1.
2874        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2875        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2876        :param expDate: string "Undefined" by default or local date in future,
2877                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2878        :return: JSON with response from broker server.
2879        """
2880        if self.accountId is None or not self.accountId:
2881            uLogger.error("Variable `accountId` must be defined for using this method!")
2882            raise Exception("Account ID required")
2883
2884        if operation is None or not operation or operation not in ("Buy", "Sell"):
2885            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2886            raise Exception("Incorrect value")
2887
2888        if lots is None or lots < 1:
2889            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2890            lots = 1
2891
2892        if tp is None or tp < 0:
2893            tp = 0
2894
2895        if sl is None or sl < 0:
2896            sl = 0
2897
2898        if expDate is None or not expDate:
2899            expDate = "Undefined"
2900
2901        if not (self.ticker or self.figi):
2902            uLogger.error("Ticker or FIGI must be defined!")
2903            raise Exception("Ticker or FIGI required")
2904
2905        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
2906        self.ticker = instrument["ticker"]
2907        self.figi = instrument["figi"]
2908
2909        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2910
2911        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2912        self.body = str({
2913            "figi": self.figi,
2914            "quantity": str(lots),
2915            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2916            "accountId": str(self.accountId),
2917            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2918        })
2919        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False)
2920
2921        if "orderId" in response.keys():
2922            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2923                operation, response["orderId"],
2924                self.ticker, self.figi, lots,
2925                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2926                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2927                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2928            ))
2929
2930        else:
2931            uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.")
2932
2933        if tp > 0:
2934            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2935
2936        if sl > 0:
2937            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2938
2939        return response
2940
2941    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2942        """
2943        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2944        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2945
2946        See also: `Order()` and `Trade()` docstrings.
2947
2948        :param lots: volume, integer count of lots >= 1.
2949        :param tp: float > 0, take profit price of stop-order.
2950        :param sl: float > 0, stop loss price of stop-order.
2951        :param expDate: it's a local date in future.
2952                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2953        :return: JSON with response from broker server.
2954        """
2955        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
2956
2957    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2958        """
2959        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2960        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2961
2962        See also: `Order()` and `Trade()` docstrings.
2963
2964        :param lots: volume, integer count of lots >= 1.
2965        :param tp: float > 0, take profit price of stop-order.
2966        :param sl: float > 0, stop loss price of stop-order.
2967        :param expDate: it's a local date in the future.
2968                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2969        :return: JSON with response from broker server.
2970        """
2971        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
2972
2973    def CloseTrades(self, tickers: list, portfolio: dict = None) -> None:
2974        """
2975        Close position of given instruments.
2976
2977        :param tickers: tickers list of instruments that must be closed.
2978        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
2979                         This avoids unnecessary downloading data from the server.
2980        """
2981        if not tickers:
2982            uLogger.info("Tickers list is empty, nothing to close.")
2983
2984        else:
2985            if portfolio is None or not portfolio:
2986                portfolio = self.Overview(show=False)
2987
2988            allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
2989            uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers))
2990
2991            for ticker in tickers:
2992                if ticker not in allOpenedTickers:
2993                    uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker))
2994                    continue
2995
2996                # search open trade info about instrument by ticker:
2997                instrument = {}
2998                for iType in TKS_INSTRUMENTS:
2999                    if instrument:
3000                        break
3001
3002                    for item in portfolio["stat"][iType]:
3003                        if item["ticker"] == ticker:
3004                            instrument = item
3005                            break
3006
3007                if instrument:
3008                    self.ticker = ticker
3009                    self.figi = instrument["figi"]
3010
3011                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3012                        self.ticker,
3013                        self.figi,
3014                        int(instrument["volume"]),
3015                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3016                    ))
3017
3018                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3019
3020                    if tradeLots > 0:
3021                        if instrument["blocked"] > 0:
3022                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3023                                instrument["blocked"],
3024                                self.ticker,
3025                                tradeLots,
3026                            ))
3027
3028                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3029                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3030
3031                    else:
3032                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
3033
3034    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3035        """
3036        Close all positions of given instruments with defined type.
3037
3038        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3039        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3040                         This avoids unnecessary downloading data from the server.
3041        """
3042        if iType not in TKS_INSTRUMENTS:
3043            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3044
3045        else:
3046            if portfolio is None or not portfolio:
3047                portfolio = self.Overview(show=False)
3048
3049            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3050            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3051
3052            if tickers and portfolio:
3053                self.CloseTrades(tickers, portfolio)
3054
3055            else:
3056                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3057
3058    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3059        """
3060        Universal method to create market or limit orders with all available parameters for current `accountId`.
3061        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3062
3063        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3064        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3065
3066        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3067        then broker immediately open market order as you can do simple --buy or --sell operations!
3068
3069        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3070        When current price will go up or down to target price value then broker opens a limit order.
3071        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3072
3073        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3074
3075        :param operation: string "Buy" or "Sell".
3076        :param orderType: string "Limit" or "Stop".
3077        :param lots: volume, integer count of lots >= 1.
3078        :param targetPrice: target price > 0. This is open trade price for limit order.
3079        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3080                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3081        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3082                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3083                         Stop loss order always executed by market price.
3084        :param expDate: string "Undefined" by default or local date in future.
3085                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3086                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3087                        A limit order has no expiration date, it lasts until the end of the trading day.
3088        :return: JSON with response from broker server.
3089        """
3090        if self.accountId is None or not self.accountId:
3091            uLogger.error("Variable `accountId` must be defined for using this method!")
3092            raise Exception("Account ID required")
3093
3094        if operation is None or not operation or operation not in ("Buy", "Sell"):
3095            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3096            raise Exception("Incorrect value")
3097
3098        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3099            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3100            raise Exception("Incorrect value")
3101
3102        if lots is None or lots < 1:
3103            uLogger.error("You must define trade volume > 0: integer count of lots!")
3104            raise Exception("Incorrect value")
3105
3106        if targetPrice is None or targetPrice <= 0:
3107            uLogger.error("Target price for limit-order must be greater than 0!")
3108            raise Exception("Incorrect value")
3109
3110        if limitPrice is None or limitPrice <= 0:
3111            limitPrice = targetPrice
3112
3113        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3114            stopType = "Limit"
3115
3116        if expDate is None or not expDate:
3117            expDate = "Undefined"
3118
3119        if not (self.ticker or self.figi):
3120            uLogger.error("Tocker or FIGI must be defined!")
3121            raise Exception("Ticker or FIGI required")
3122
3123        response = {}
3124        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
3125        self.ticker = instrument["ticker"]
3126        self.figi = instrument["figi"]
3127
3128        if orderType == "Limit":
3129            uLogger.debug(
3130                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3131                    self.ticker, self.figi,
3132                    operation, lots, targetPrice, instrument["currency"],
3133                ))
3134
3135            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3136            self.body = str({
3137                "figi": self.figi,
3138                "quantity": str(lots),
3139                "price": FloatToNano(targetPrice),
3140                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3141                "accountId": str(self.accountId),
3142                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3143            })
3144            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3145
3146            if "orderId" in response.keys():
3147                uLogger.info(
3148                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3149                        response["orderId"],
3150                        self.ticker, self.figi,
3151                        operation, lots, targetPrice, instrument["currency"],
3152                    ))
3153
3154                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3155                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3156                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3157                            targetPrice, instrument["currency"],
3158                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3159                        ))
3160
3161                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3162                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3163                            targetPrice, instrument["currency"],
3164                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3165                        ))
3166
3167            else:
3168                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3169
3170        if orderType == "Stop":
3171            uLogger.debug(
3172                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3173                    self.ticker, self.figi,
3174                    operation, lots,
3175                    targetPrice, instrument["currency"],
3176                    limitPrice, instrument["currency"],
3177                    stopType, expDate,
3178                ))
3179
3180            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3181            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3182            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3183
3184            body = {
3185                "figi": self.figi,
3186                "quantity": str(lots),
3187                "price": FloatToNano(limitPrice),
3188                "stopPrice": FloatToNano(targetPrice),
3189                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3190                "accountId": str(self.accountId),
3191                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3192                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3193            }
3194
3195            if expDateUTC:
3196                body["expireDate"] = expDateUTC
3197
3198            self.body = str(body)
3199            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3200
3201            if "stopOrderId" in response.keys():
3202                uLogger.info(
3203                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3204                        response["stopOrderId"],
3205                        self.ticker, self.figi,
3206                        operation, lots,
3207                        targetPrice, instrument["currency"],
3208                        limitPrice, instrument["currency"],
3209                        TKS_STOP_ORDER_TYPES[stopOrderType],
3210                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3211                    ))
3212
3213                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3214                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3215                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3216                            targetPrice, instrument["currency"],
3217                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3218                        ))
3219
3220                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3221                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3222                            targetPrice, instrument["currency"],
3223                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3224                        ))
3225
3226            else:
3227                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3228
3229        return response
3230
3231    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3232        """
3233        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3234        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3235        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3236        See also: `Order()` docstring.
3237
3238        :param lots: volume, integer count of lots >= 1.
3239        :param targetPrice: target price > 0. This is open trade price for limit order.
3240        :return: JSON with response from broker server.
3241        """
3242        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3243
3244    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3245        """
3246        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3247        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3248        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3249        target price value then broker opens a limit order. See also: `Order()` docstring.
3250
3251        :param lots: volume, integer count of lots >= 1.
3252        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3253        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3254                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3255        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3256                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3257        :param expDate: string "Undefined" by default or local date in future.
3258                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3259                        This date is converting to UTC format for server.
3260        :return: JSON with response from broker server.
3261        """
3262        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3263
3264    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3265        """
3266        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3267        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3268        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3269        See also: `Order()` docstring.
3270
3271        :param lots: volume, integer count of lots >= 1.
3272        :param targetPrice: target price > 0. This is open trade price for limit order.
3273        :return: JSON with response from broker server.
3274        """
3275        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3276
3277    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3278        """
3279        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3280        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3281        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3282        target price value then broker opens a limit order. See also: `Order()` docstring.
3283
3284        :param lots: volume, integer count of lots >= 1.
3285        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3286        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3287                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3288        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3289                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3290        :param expDate: string "Undefined" by default or local date in future.
3291                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3292                        This date is converting to UTC format for server.
3293        :return: JSON with response from broker server.
3294        """
3295        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3296
3297    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3298        """
3299        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3300
3301        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3302        :param allOrdersIDs: pre-received lists of all active pending orders.
3303                             This avoids unnecessary downloading data from the server.
3304        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3305        """
3306        if self.accountId is None or not self.accountId:
3307            uLogger.error("Variable `accountId` must be defined for using this method!")
3308            raise Exception("Account ID required")
3309
3310        if orderIDs:
3311            if allOrdersIDs is None or not allOrdersIDs:
3312                rawOrders = self.RequestPendingOrders()
3313                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3314
3315            if allStopOrdersIDs is None or not allStopOrdersIDs:
3316                rawStopOrders = self.RequestStopOrders()
3317                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3318
3319            for orderID in orderIDs:
3320                idInPendingOrders = orderID in allOrdersIDs
3321                idInStopOrders = orderID in allStopOrdersIDs
3322
3323                if not (idInPendingOrders or idInStopOrders):
3324                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3325                    continue
3326
3327                else:
3328                    if idInPendingOrders:
3329                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3330
3331                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3332                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3333                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3334                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3335
3336                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3337                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3338                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3339
3340                        else:
3341                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3342
3343                    elif idInStopOrders:
3344                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3345
3346                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3347                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3348                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3349                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3350
3351                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3352                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3353                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3354
3355                        else:
3356                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3357
3358                    else:
3359                        continue
3360
3361    def CloseAllOrders(self) -> None:
3362        """
3363        Gets a list of open pending and stop orders and cancel it all.
3364        """
3365        rawOrders = self.RequestPendingOrders()
3366        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3367        lenOrders = len(allOrdersIDs)
3368
3369        rawStopOrders = self.RequestStopOrders()
3370        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3371        lenSOrders = len(allStopOrdersIDs)
3372
3373        if lenOrders > 0 or lenSOrders > 0:
3374            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3375
3376            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3377
3378        else:
3379            uLogger.info("Orders not found, nothing to cancel.")
3380
3381    def CloseAll(self, *args) -> None:
3382        """
3383        Close all available (not blocked) opened trades and orders.
3384
3385        Also, you can select one or more keywords case-insensitive:
3386        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3387
3388        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3389        """
3390        overview = self.Overview(show=False)  # get all open trades info
3391
3392        if len(args) == 0:
3393            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3394            self.CloseAllOrders()  # close all pending and stop orders
3395
3396            for iType in TKS_INSTRUMENTS:
3397                if iType != "Currencies":
3398                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3399
3400        else:
3401            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3402            lowerArgs = [x.lower() for x in args]
3403
3404            if "orders" in lowerArgs:
3405                self.CloseAllOrders()  # close all pending and stop orders
3406
3407            for iType in TKS_INSTRUMENTS:
3408                if iType.lower() in lowerArgs and iType != "Currencies":
3409                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3410
3411    @staticmethod
3412    def ParseOrderParameters(operation, **inputParameters):
3413        """
3414        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3415
3416        :param operation: string "Buy" or "Sell".
3417        :param inputParameters: this is dict of strings that looks like this
3418               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3419               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3420               "prices" key: one or more prices to open limit-orders
3421               Counts of values in lots and prices lists must be equals!
3422        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3423        """
3424        # TODO: update order grid work with api v2
3425        pass
3426        # uLogger.debug("Input parameters: {}".format(inputParameters))
3427        #
3428        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3429        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3430        #     raise Exception("Incorrect value")
3431        #
3432        # if "l" in inputParameters.keys():
3433        #     inputParameters["lots"] = inputParameters.pop("l")
3434        #
3435        # if "p" in inputParameters.keys():
3436        #     inputParameters["prices"] = inputParameters.pop("p")
3437        #
3438        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3439        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3440        #     raise Exception("Incorrect value")
3441        #
3442        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3443        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3444        #
3445        # if len(lots) != len(prices):
3446        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3447        #     raise Exception("Incorrect value")
3448        #
3449        # uLogger.debug("Extracted parameters for orders:")
3450        # uLogger.debug("lots = {}".format(lots))
3451        # uLogger.debug("prices = {}".format(prices))
3452        #
3453        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3454        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3455        # uLogger.debug("Order parameters: {}".format(result))
3456        #
3457        # return result
3458
3459    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3460        """
3461        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3462
3463        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3464        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3465        """
3466        result = False
3467        msg = "Instrument not defined!"
3468
3469        if portfolio is None or not portfolio:
3470            portfolio = self.Overview(show=False)
3471
3472        if self.ticker:
3473            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3474            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3475
3476            for iType in TKS_INSTRUMENTS:
3477                for instrument in portfolio["stat"][iType]:
3478                    if instrument["ticker"] == self.ticker:
3479                        result = True
3480                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3481                        break
3482
3483        elif self.figi:
3484            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3485            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3486
3487            for iType in TKS_INSTRUMENTS:
3488                for instrument in portfolio["stat"][iType]:
3489                    if instrument["figi"] == self.figi:
3490                        result = True
3491                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3492                        break
3493
3494        else:
3495            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3496
3497        uLogger.debug(msg)
3498
3499        return result
3500
3501    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3502        """
3503        Returns instrument is in the user's portfolio if it presents there.
3504        Instrument must be defined by `ticker` (highly priority) or `figi`.
3505
3506        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3507        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3508        """
3509        result = None
3510        msg = "Instrument not defined!"
3511
3512        if portfolio is None or not portfolio:
3513            portfolio = self.Overview(show=False)
3514
3515        if self.ticker:
3516            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3517            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3518
3519            for iType in TKS_INSTRUMENTS:
3520                for instrument in portfolio["stat"][iType]:
3521                    if instrument["ticker"] == self.ticker:
3522                        result = instrument
3523                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3524                        break
3525
3526        elif self.figi:
3527            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3528            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3529
3530            for iType in TKS_INSTRUMENTS:
3531                for instrument in portfolio["stat"][iType]:
3532                    if instrument["figi"] == self.figi:
3533                        result = instrument
3534                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3535                        break
3536
3537        else:
3538            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3539
3540        uLogger.debug(msg)
3541
3542        return result
3543
3544    def RequestLimits(self) -> dict:
3545        """
3546        Method for obtaining the available funds for withdrawal for current `accountId`.
3547
3548        See also:
3549        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3550        - `OverviewLimits()` method
3551
3552        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3553                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3554                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3555                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3556        """
3557        if self.accountId is None or not self.accountId:
3558            uLogger.error("Variable `accountId` must be defined for using this method!")
3559            raise Exception("Account ID required")
3560
3561        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3562
3563        self.body = str({"accountId": self.accountId})
3564        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3565        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3566
3567        uLogger.debug("Records about available funds for withdrawal successfully received")
3568
3569        return rawLimits
3570
3571    def OverviewLimits(self, show: bool = False) -> dict:
3572        """
3573        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3574
3575        See also: `RequestLimits()`.
3576
3577        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3578        :return: dict with raw parsed data from server and some calculated statistics about it.
3579        """
3580        if self.accountId is None or not self.accountId:
3581            uLogger.error("Variable `accountId` must be defined for using this method!")
3582            raise Exception("Account ID required")
3583
3584        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3585
3586        view = {
3587            "rawLimits": rawLimits,
3588            "limits": {  # parsed data for every currency:
3589                "money": {  # this is an array of portfolio currency positions
3590                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3591                },
3592                "blocked": {  # this is an array of blocked currency
3593                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3594                },
3595                "blockedGuarantee": {  # this is locked money under collateral for futures
3596                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3597                },
3598            },
3599        }
3600
3601        # --- Prepare text table with limits in human-readable format:
3602        if show:
3603            info = [
3604                "# Withdrawal limits\n\n",
3605                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3606                "* **Account ID:** [{}]\n".format(self.accountId),
3607                "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3608                "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3609            ]
3610
3611            for curr in view["limits"]["money"].keys():
3612                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3613                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3614                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3615
3616                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3617                    "[{}]".format(curr),
3618                    "{:.2f}".format(view["limits"]["money"][curr]),
3619                    "{:.2f}".format(availableMoney),
3620                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3621                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3622                )
3623
3624                if curr == "rub":
3625                    info.insert(5, infoStr)  # insert at first position in table and after headers
3626
3627                else:
3628                    info.append(infoStr)
3629
3630            infoText = "".join(info)
3631
3632            uLogger.info(infoText)
3633
3634            if self.withdrawalLimitsFile:
3635                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3636                    fH.write(infoText)
3637
3638                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3639
3640        return view
3641
3642    def RequestAccounts(self) -> dict:
3643        """
3644        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3645
3646        See also:
3647        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3648        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3649        - `OverviewUserInfo()` method
3650
3651        :return: dict with raw data from server that contains accounts info. Example of dict:
3652                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3653                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3654                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3655                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3656        """
3657        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3658
3659        self.body = str({})
3660        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3661        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3662
3663        uLogger.debug("Records about available accounts successfully received")
3664
3665        return rawAccounts
3666
3667    def RequestUserInfo(self) -> dict:
3668        """
3669        Method for requesting common user's information.
3670
3671        See also:
3672        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3673        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3674        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3675        - `OverviewUserInfo()` method
3676
3677        :return: dict with raw data from server that contains user's information. Example of dict:
3678                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3679                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3680        """
3681        uLogger.debug("Requesting common user's information. Wait, please...")
3682
3683        self.body = str({})
3684        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3685        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3686
3687        uLogger.debug("Records about current user successfully received")
3688
3689        return rawUserInfo
3690
3691    def RequestMarginStatus(self, accountId: str = None) -> dict:
3692        """
3693        Method for requesting margin calculation for defined account ID.
3694
3695        See also:
3696        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3697        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3698        - `OverviewUserInfo()` method
3699
3700        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3701        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3702                 Example of responses:
3703                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3704                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3705                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3706                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3707                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3708                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3709        """
3710        if accountId is None or not accountId:
3711            if self.accountId is None or not self.accountId:
3712                uLogger.error("Variable `accountId` must be defined for using this method!")
3713                raise Exception("Account ID required")
3714
3715            else:
3716                accountId = self.accountId  # use `self.accountId` (main ID) by default
3717
3718        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3719
3720        self.body = str({"accountId": accountId})
3721        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3722        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3723
3724        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3725            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3726            rawMargin = {}
3727
3728        else:
3729            uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3730
3731        return rawMargin
3732
3733    def RequestTariffLimits(self) -> dict:
3734        """
3735        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3736
3737        See also:
3738        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3739        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3740        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3741        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3742        - `OverviewUserInfo()` method
3743
3744        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3745                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3746                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3747        """
3748        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3749
3750        self.body = str({})
3751        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3752        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3753
3754        uLogger.debug("Records with limits of current tariff successfully received")
3755
3756        return rawTariffLimits
3757
3758    def RequestBondCoupons(self, iJSON: dict) -> dict:
3759        """
3760        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3761        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3762        All dates are in UTC timezone.
3763
3764        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3765        Documentation:
3766        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3767        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3768
3769        See also: `ExtendBondsData()`.
3770
3771        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3772                      If raw iJSON is not data of bond then server returns an error [400] with message:
3773                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3774        :return: dictionary with bond payment calendar. Response example
3775                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3776                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3777                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3778                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3779        """
3780        if iJSON["figi"] is None or not iJSON["figi"]:
3781            uLogger.error("FIGI must be defined for using this method!")
3782            raise Exception("FIGI required")
3783
3784        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3785        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3786
3787        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3788            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3789            self.figi,
3790            startDate,
3791            endDate,
3792        ))
3793
3794        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3795        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3796        calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False)
3797
3798        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3799            uLogger.warning("Instrument type is not bond!")
3800
3801        else:
3802            uLogger.debug("Records about bond payment calendar successfully received")
3803
3804        return calendar
3805
3806    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3807        """
3808        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3809        pandas dataframe with more information about bonds: main info, current prices, bond payment calendar,
3810        coupon yields, current yields and some statistics etc.
3811
3812        WARNING! This is too long operation if a lot of bonds requested from broker server.
3813
3814        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3815
3816        :param instruments: list of strings with tickers or FIGIs.
3817        :param xlsx: if True then also exports pandas dataframe to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3818                     for further used by data scientists or stock analytics.
3819        :return: wider pandas dataframe with more full and calculated data about bonds, than raw response from broker.
3820                 In XLSX-file and pandas dataframe fields mean:
3821                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3822                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3823        """
3824        if instruments is None or not instruments:
3825            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3826            raise Exception("Ticker or FIGI required")
3827
3828        if isinstance(instruments, str):
3829            instruments = [instruments]
3830
3831        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3832
3833        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3834
3835        iCount = len(uniqueInstruments)
3836        tooLong = iCount >= 20
3837        if tooLong:
3838            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3839
3840        bonds = None
3841        for i, self.figi in enumerate(uniqueInstruments):
3842            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3843
3844            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3845                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3846                rawBond = self.SearchByFIGI(requestPrice=True)
3847
3848                # Widen raw data with UTC current time (iData["actualDateTime"]):
3849                actualDate = datetime.now(tzutc())
3850                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3851
3852                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3853                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3854
3855                # Replace some values with human-readable:
3856                iData["nominalCurrency"] = iData["nominal"]["currency"]
3857                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3858                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3859                iData["aciCurrency"] = iData["aciValue"]["currency"]
3860                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3861                iData["issueSize"] = int(iData["issueSize"])
3862                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3863                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3864                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3865                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3866                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3867                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3868                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3869                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3870                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3871                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3872
3873                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3874                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3875                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3876                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3877                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3878                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3879                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3880                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3881                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3882                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3883                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3884
3885                # Widen raw data with calendar data from `rawCalendar` values:
3886                calendarData = []
3887                for item in iData["rawCalendar"]["events"]:
3888                    calendarData.append({
3889                        "couponDate": item["couponDate"],
3890                        "couponNumber": int(item["couponNumber"]),
3891                        "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3892                        "payCurrency": item["payOneBond"]["currency"],
3893                        "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3894                        "couponType": TKS_COUPON_TYPES[item["couponType"]],
3895                        "couponStartDate": item["couponStartDate"],
3896                        "couponEndDate": item["couponEndDate"],
3897                        "couponPeriod": item["couponPeriod"],
3898                    })
3899
3900                # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3901                if "maturityDate" not in iData.keys():
3902                    iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3903
3904                # Widen raw data with Coupon Rate.
3905                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3906                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3907                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3908                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3909
3910                # Widen raw data with Yield to Maturity (YTM) on current date.
3911                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3912                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3913                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3914                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3915                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3916                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3917
3918                iData["calendar"] = calendarData  # adds calendar at the end
3919
3920                # Remove not used data:
3921                iData.pop("uid")
3922                iData.pop("positionUid")
3923                iData.pop("currentPrice")
3924                iData.pop("rawCalendar")
3925
3926                colNames = list(iData.keys())
3927                if bonds is None:
3928                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3929
3930                else:
3931                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
3932
3933            else:
3934                uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"]))
3935
3936            processed = round(100 * (i + 1) / iCount, 1)
3937            if tooLong and processed % 5 == 0:
3938                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
3939
3940            else:
3941                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
3942
3943        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
3944
3945        # Saving bonds from pandas dataframe to XLSX sheet:
3946        if xlsx and self.bondsXLSXFile:
3947            with pd.ExcelWriter(
3948                    path=self.bondsXLSXFile,
3949                    date_format=TKS_DATE_FORMAT,
3950                    datetime_format=TKS_DATE_TIME_FORMAT,
3951                    mode="w",
3952            ) as writer:
3953                bonds.to_excel(
3954                    writer,
3955                    sheet_name="Extended bonds data",
3956                    index=True,
3957                    encoding="UTF-8",
3958                    freeze_panes=(1, 1),
3959                )  # saving as XLSX-file with freeze first row and column as headers
3960
3961            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
3962
3963        return bonds
3964
3965    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
3966        """
3967        Creates bond payments calendar as pandas dataframe, and also save it to the XLSX-file, `calendar.xlsx` by default.
3968
3969        WARNING! This is too long operation if a lot of bonds requested from broker server.
3970
3971        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
3972
3973        :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains
3974                        extended information about bonds: main info, current prices, bond payment calendar,
3975                        coupon yields, current yields and some statistics etc.
3976                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
3977        :param xlsx: if True then also exports pandas dataframe to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
3978                     for further used by data scientists or stock analytics.
3979        :return: pandas dataframe with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
3980        """
3981        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
3982            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
3983
3984        uLogger.debug("Generating bond payments calendar data. Wait, please...")
3985
3986        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
3987        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
3988        calendar = None
3989        for bond in extBonds.iterrows():
3990            for item in bond[1]["calendar"]:
3991                cData = {
3992                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
3993                    "couponDate": item["couponDate"],
3994                    "figi": bond[1]["figi"],
3995                    "ticker": bond[1]["ticker"],
3996                    "name": bond[1]["name"],
3997                    "couponNumber": item["couponNumber"],
3998                    "payOneBond": item["payOneBond"],
3999                    "payCurrency": item["payCurrency"],
4000                    "couponType": item["couponType"],
4001                    "couponPeriod": item["couponPeriod"],
4002                    "fixDate": item["fixDate"],
4003                    "couponStartDate": item["couponStartDate"],
4004                    "couponEndDate": item["couponEndDate"],
4005                }
4006
4007                if calendar is None:
4008                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4009
4010                else:
4011                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4012
4013        calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4014
4015        # Saving calendar from pandas dataframe to XLSX sheet:
4016        if xlsx:
4017            xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4018
4019            with pd.ExcelWriter(
4020                    path=xlsxCalendarFile,
4021                    date_format=TKS_DATE_FORMAT,
4022                    datetime_format=TKS_DATE_TIME_FORMAT,
4023                    mode="w",
4024            ) as writer:
4025                humanReadable = calendar.copy(deep=True)
4026                humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4027                humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4028                humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4029                humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4030                humanReadable.columns = colNames  # human-readable column names
4031
4032                humanReadable.to_excel(
4033                    writer,
4034                    sheet_name="Bond payments calendar",
4035                    index=False,
4036                    encoding="UTF-8",
4037                    freeze_panes=(1, 2),
4038                )  # saving as XLSX-file with freeze first row and column as headers
4039
4040                del humanReadable  # release df in memory
4041
4042            uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4043
4044        return calendar
4045
4046    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4047        """
4048        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4049        Also, creates Markdown file with calendar data, `calendar.md` by default.
4050
4051        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4052
4053        :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains
4054                        extended information about bonds: main info, current prices, bond payment calendar,
4055                        coupon yields, current yields and some statistics etc.
4056                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4057        :param show: if `True` then also printing bonds payment calendar to the console,
4058                     otherwise save to file `calendarFile` only. `False` by default.
4059        :return: multilines text in Markdown format with bonds payment calendar as a table.
4060        """
4061        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4062            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4063
4064        infoText = "# Bond payments calendar\n\n"
4065
4066        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate pandas dataframe with full calendar data
4067
4068        if not calendar.empty:
4069            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4070
4071            info = [
4072                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4073                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4074            ]
4075
4076            newMonth = False
4077            notOneBond = calendar["figi"].nunique() > 1
4078            for i, bond in enumerate(calendar.iterrows()):
4079                if newMonth and notOneBond:
4080                    info.append(splitLine)
4081
4082                info.append(
4083                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4084                        "  +" if bond[1]["paid"] else "  —",
4085                        bond[1]["couponDate"].split("T")[0],
4086                        bond[1]["figi"],
4087                        bond[1]["ticker"],
4088                        bond[1]["couponNumber"],
4089                        "{} {}".format(
4090                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4091                            bond[1]["payCurrency"],
4092                        ),
4093                        bond[1]["couponType"],
4094                        bond[1]["couponPeriod"],
4095                        bond[1]["fixDate"].split("T")[0],
4096                    )
4097                )
4098
4099                if i < len(calendar.values) - 1:
4100                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4101                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4102                    newMonth = False if curDate.month == nextDate.month else True
4103
4104                else:
4105                    newMonth = False
4106
4107            infoText += "".join(info)
4108
4109            if show:
4110                uLogger.info("{}".format(infoText))
4111
4112            if self.calendarFile is not None:
4113                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4114                    fH.write(infoText)
4115
4116                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4117
4118        else:
4119            infoText += "No data\n"
4120
4121        return infoText
4122
4123    def OverviewAccounts(self, show: bool = False) -> dict:
4124        """
4125        Method for parsing and show simple table with all available user accounts.
4126
4127        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4128
4129        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4130        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4131                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4132                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4133                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4134                                                        "closed": "—", "access": "Full access" }, ...}}`
4135        """
4136        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4137
4138        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4139        accounts = {
4140            item["id"]: {
4141                "type": TKS_ACCOUNT_TYPES[item["type"]],
4142                "name": item["name"],
4143                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4144                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4145                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4146                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4147            } for item in rawAccounts["accounts"]
4148        }
4149
4150        # Raw and parsed data with some fields replaced in "stat" section:
4151        view = {
4152            "rawAccounts": rawAccounts,
4153            "stat": accounts,
4154        }
4155
4156        # --- Prepare simple text table with only accounts data in human-readable format:
4157        if show:
4158            info = [
4159                "# User accounts\n\n",
4160                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4161                "| Account ID   | Type                      | Status                    | Name                           |\n",
4162                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4163            ]
4164
4165            for account in view["stat"].keys():
4166                info.extend([
4167                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4168                        account,
4169                        view["stat"][account]["type"],
4170                        view["stat"][account]["status"],
4171                        view["stat"][account]["name"],
4172                    )
4173                ])
4174
4175            infoText = "".join(info)
4176
4177            uLogger.info(infoText)
4178
4179            if self.userAccountsFile:
4180                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4181                    fH.write(infoText)
4182
4183                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4184
4185        return view
4186
4187    def OverviewUserInfo(self, show: bool = False) -> dict:
4188        """
4189        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4190
4191        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4192
4193        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4194        :return: dict with raw parsed data from server and some calculated statistics about it.
4195        """
4196        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4197        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4198        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4199        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4200        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4201        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4202
4203        # This is dict with parsed common user data:
4204        userInfo = {
4205            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4206            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4207            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4208            "tariff": rawUserInfo["tariff"],
4209        }
4210
4211        # This is an array of dict with parsed margin statuses for every account IDs:
4212        margins = {}
4213        for accountId in accounts.keys():
4214            if rawMargins[accountId]:
4215                margins[accountId] = {
4216                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4217                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4218                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4219                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4220                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4221                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4222                }
4223
4224            else:
4225                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4226
4227        unary = {}  # unary-connection limits
4228        for item in rawTariffLimits["unaryLimits"]:
4229            if item["limitPerMinute"] in unary.keys():
4230                unary[item["limitPerMinute"]].extend(item["methods"])
4231
4232            else:
4233                unary[item["limitPerMinute"]] = item["methods"]
4234
4235        stream = {}  # stream-connection limits
4236        for item in rawTariffLimits["streamLimits"]:
4237            if item["limit"] in stream.keys():
4238                stream[item["limit"]].extend(item["streams"])
4239
4240            else:
4241                stream[item["limit"]] = item["streams"]
4242
4243        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4244        limits = {
4245            "unary": unary,
4246            "stream": stream,
4247        }
4248
4249        # Raw and parsed data as an output result:
4250        view = {
4251            "rawUserInfo": rawUserInfo,
4252            "rawAccounts": rawAccounts,
4253            "rawMargins": rawMargins,
4254            "rawTariffLimits": rawTariffLimits,
4255            "stat": {
4256                "userInfo": userInfo,
4257                "accounts": accounts,
4258                "margins": margins,
4259                "limits": limits,
4260            },
4261        }
4262
4263        # --- Prepare text table with user information in human-readable format:
4264        if show:
4265            info = [
4266                "# Full user information\n\n",
4267                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4268                "## Common information\n\n",
4269                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4270                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4271                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4272                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4273                "\n## User accounts\n\n",
4274            ]
4275
4276            for account in view["stat"]["accounts"].keys():
4277                info.extend([
4278                    "### ID: [{}]\n\n".format(account),
4279                    "| Parameters           | Values                                                       |\n",
4280                    "|----------------------|--------------------------------------------------------------|\n",
4281                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4282                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4283                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4284                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4285                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4286                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4287                ])
4288
4289                if margins[account]:
4290                    info.extend([
4291                        "| Margin status:       | Enabled                                                      |\n",
4292                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4293                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4294                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4295                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4296                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4297                    ])
4298
4299                else:
4300                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4301
4302            info.extend([
4303                "\n## Current user tariff limits\n",
4304                "\nSee also:\n",
4305                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4306                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4307                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4308                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4309                "\n### Unary limits\n",
4310            ])
4311
4312            if unary:
4313                for key, values in sorted(unary.items()):
4314                    info.append("\n* Max requests per minute: {}\n".format(key))
4315
4316                    for value in values:
4317                        info.append("  - {}\n".format(value))
4318
4319            else:
4320                info.append("\nNot available\n")
4321
4322            info.append("\n### Stream limits\n")
4323
4324            if stream:
4325                for key, values in sorted(stream.items()):
4326                    info.append("\n* Max stream connections: {}\n".format(key))
4327
4328                    for value in values:
4329                        info.append("  - {}\n".format(value))
4330
4331            else:
4332                info.append("\nNot available\n")
4333
4334            infoText = "".join(info)
4335
4336            uLogger.info(infoText)
4337
4338            if self.userInfoFile:
4339                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4340                    fH.write(infoText)
4341
4342                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4343
4344        return view
4345
4346
4347class Args:
4348    """
4349    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4350    """
4351    def __init__(self, **kwargs):
4352        self.__dict__.update(kwargs)
4353
4354    def __getattr__(self, item):
4355        return None
4356
4357
4358def ParseArgs():
4359    """
4360    Function get and parse command line keys.
4361
4362    See examples:
4363    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4364    - in russian: https://tim55667757.github.io/TKSBrokerAPI/
4365    """
4366    parser = ArgumentParser()  # command-line string parser
4367
4368    parser.description = "TKSBrokerAPI is a python API to work with some methods of Tinkoff Open API using REST protocol. It can view history, orders and market information. Also, you can open orders and trades. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples"
4369    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4370
4371    # --- options:
4372
4373    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the program. `False` by default.")
4374    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4375    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4376
4377    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4378    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4379
4380    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4381    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4382
4383    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4384
4385    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4386    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4387    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4388
4389    parser.add_argument("--debug-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4390
4391    # --- commands:
4392
4393    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4394
4395    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4396    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4397    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider pandas dataframe with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4398    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4399    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4400    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4401    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4402    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4403
4404    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4405    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4406    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4407    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4408    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4409
4410    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4411    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4412    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4413    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4414
4415    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4416    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4417    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4418
4419    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4420    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4421    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4422    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4423    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4424    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4425    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4426
4427    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4428    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4429    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` key, including for currencies tickers.")
4430    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers, including for currencies tickers.")
4431    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.")
4432
4433    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4434    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4435    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4436
4437    cmdArgs = parser.parse_args()
4438    return cmdArgs
4439
4440
4441def Main(**kwargs):
4442    """
4443    Main function for work with Tinkoff Open API service. It realizes simple logic: get a lot of options and execute one command.
4444
4445    See examples:
4446    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4447    - in russian: https://tim55667757.github.io/TKSBrokerAPI/
4448    """
4449    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4450
4451    if args.debug_level:
4452        uLogger.level = 10  # always debug level by default
4453        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4454
4455    exitCode = 0
4456    start = datetime.now(tzutc())
4457    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4458        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4459        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4460    ))
4461
4462    # trying to calculate full current version:
4463    buildVersion = __version__
4464    try:
4465        v = version("tksbrokerapi")
4466        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4467
4468    except Exception:
4469        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4470
4471    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4472    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4473
4474    try:
4475        if args.version:
4476            print("TKSBrokerAPI {}".format(buildVersion))
4477            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4478
4479        else:
4480            # Init class for trading with Tinkoff Broker: TODO: rename `server` to `trader`
4481            server = TinkoffBrokerServer(
4482                token=args.token,
4483                accountId=args.account_id,
4484                useCache=not args.no_cache,
4485            )
4486
4487            # --- set some options:
4488
4489            if args.ticker:
4490                if args.ticker in server.aliasesKeys:
4491                    server.ticker = server.aliases[args.ticker]  # Replace some tickers with its aliases
4492
4493                else:
4494                    server.ticker = args.ticker
4495
4496            if args.figi:
4497                server.figi = args.figi
4498
4499            if args.depth is not None:
4500                server.depth = args.depth
4501
4502            # --- do one of commands:
4503
4504            if args.list:
4505                if args.output is not None:
4506                    server.instrumentsFile = args.output
4507
4508                server.ShowInstrumentsInfo(show=True)
4509
4510            elif args.list_xlsx:
4511                server.DumpInstrumentsAsXLSX(forceUpdate=False)
4512
4513            elif args.bonds_xlsx is not None:
4514                if args.output is not None:
4515                    server.bondsXLSXFile = args.output
4516
4517                if len(args.bonds_xlsx) == 0:
4518                    server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4519
4520                else:
4521                    server.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4522
4523            elif args.search:
4524                if args.output is not None:
4525                    server.searchResultsFile = args.output
4526
4527                server.SearchInstruments(pattern=args.search[0], show=True)
4528
4529            elif args.info:
4530                if not (args.ticker or args.figi):
4531                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4532                    raise Exception("Ticker or FIGI required")
4533
4534                if args.output is not None:
4535                    server.infoFile = args.output
4536
4537                if args.ticker:
4538                    server.SearchByTicker(requestPrice=True, show=True, debug=False)  # show info and current prices by ticker name
4539
4540                else:
4541                    server.SearchByFIGI(requestPrice=True, show=True, debug=False)  # show info and current prices by FIGI id
4542
4543            elif args.calendar is not None:
4544                if args.output is not None:
4545                    server.calendarFile = args.output
4546
4547                if len(args.calendar) == 0:
4548                    bondsData = server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4549
4550                else:
4551                    bondsData = server.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4552
4553                server.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4554
4555            elif args.price:
4556                if not (args.ticker or args.figi):
4557                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4558                    raise Exception("Ticker or FIGI required")
4559
4560                server.GetCurrentPrices(show=True)
4561
4562            elif args.prices is not None:
4563                if args.output is not None:
4564                    server.pricesFile = args.output
4565
4566                server.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4567
4568            elif args.overview:
4569                if args.output is not None:
4570                    server.overviewFile = args.output
4571
4572                server.Overview(show=True, details="full")
4573
4574            elif args.overview_digest:
4575                if args.output is not None:
4576                    server.overviewDigestFile = args.output
4577
4578                server.Overview(show=True, details="digest")
4579
4580            elif args.overview_positions:
4581                if args.output is not None:
4582                    server.overviewPositionsFile = args.output
4583
4584                server.Overview(show=True, details="positions")
4585
4586            elif args.overview_orders:
4587                if args.output is not None:
4588                    server.overviewOrdersFile = args.output
4589
4590                server.Overview(show=True, details="orders")
4591
4592            elif args.overview_analytics:
4593                if args.output is not None:
4594                    server.overviewAnalyticsFile = args.output
4595
4596                server.Overview(show=True, details="analytics")
4597
4598            elif args.deals is not None:
4599                if args.output is not None:
4600                    server.reportFile = args.output
4601
4602                if 0 <= len(args.deals) < 3:
4603                    server.Deals(
4604                        start=args.deals[0] if len(args.deals) >= 1 else None,
4605                        end=args.deals[1] if len(args.deals) == 2 else None,
4606                        show=True,  # Always show deals report in console
4607                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4608                    )
4609
4610                else:
4611                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4612                    raise Exception("Incorrect value")
4613
4614            elif args.history is not None:
4615                if args.output is not None:
4616                    server.historyFile = args.output
4617
4618                if 0 <= len(args.history) < 3:
4619                    dataReceived = server.History(
4620                        start=args.history[0] if len(args.history) >= 1 else None,
4621                        end=args.history[1] if len(args.history) == 2 else None,
4622                        interval="hour" if args.interval is None or not args.interval else args.interval,
4623                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4624                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4625                        show=True,  # shows all downloaded candles in console
4626                    )
4627
4628                    if args.render_chart is not None and dataReceived is not None:
4629                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4630
4631                        server.ShowHistoryChart(
4632                            candles=dataReceived,
4633                            interact=iChart,
4634                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4635                        )
4636
4637                else:
4638                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4639                    raise Exception("Incorrect value")
4640
4641            elif args.load_history is not None:
4642                histData = server.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
4643
4644                if args.render_chart is not None and histData is not None:
4645                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4646                    server.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
4647
4648                    server.ShowHistoryChart(
4649                        candles=histData,
4650                        interact=iChart,
4651                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4652                    )
4653
4654            elif args.trade is not None:
4655                if 1 <= len(args.trade) <= 5:
4656                    server.Trade(
4657                        operation=args.trade[0],
4658                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
4659                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
4660                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
4661                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
4662                    )
4663
4664                else:
4665                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4666
4667            elif args.buy is not None:
4668                if 0 <= len(args.buy) <= 4:
4669                    server.Buy(
4670                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
4671                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
4672                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
4673                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
4674                    )
4675
4676                else:
4677                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4678
4679            elif args.sell is not None:
4680                if 0 <= len(args.sell) <= 4:
4681                    server.Sell(
4682                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
4683                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
4684                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
4685                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
4686                    )
4687
4688                else:
4689                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4690
4691            elif args.order:
4692                if 4 <= len(args.order) <= 7:
4693                    server.Order(
4694                        operation=args.order[0],
4695                        orderType=args.order[1],
4696                        lots=int(args.order[2]),
4697                        targetPrice=float(args.order[3]),
4698                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
4699                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
4700                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
4701                    )
4702
4703                else:
4704                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
4705
4706            elif args.buy_limit:
4707                server.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
4708
4709            elif args.sell_limit:
4710                server.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
4711
4712            elif args.buy_stop:
4713                if 2 <= len(args.buy_stop) <= 7:
4714                    server.BuyStop(
4715                        lots=int(args.buy_stop[0]),
4716                        targetPrice=float(args.buy_stop[1]),
4717                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
4718                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
4719                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
4720                    )
4721
4722                else:
4723                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4724
4725            elif args.sell_stop:
4726                if 2 <= len(args.sell_stop) <= 7:
4727                    server.SellStop(
4728                        lots=int(args.sell_stop[0]),
4729                        targetPrice=float(args.sell_stop[1]),
4730                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
4731                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
4732                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
4733                    )
4734
4735                else:
4736                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
4737
4738            # elif args.buy_order_grid is not None:
4739            #     # update order grid work with api v2
4740            #     if len(args.buy_order_grid) == 2:
4741            #         orderParams = server.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
4742            #
4743            #         for order in orderParams:
4744            #             server.Order(operation="Buy", lots=order["lot"], price=order["price"])
4745            #
4746            #     else:
4747            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4748            #
4749            # elif args.sell_order_grid is not None:
4750            #     # update order grid work with api v2
4751            #     if len(args.sell_order_grid) >= 2:
4752            #         orderParams = server.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
4753            #
4754            #         for order in orderParams:
4755            #             server.Order(operation="Sell", lots=order["lot"], price=order["price"])
4756            #
4757            #     else:
4758            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4759
4760            elif args.close_order is not None:
4761                server.CloseOrders(args.close_order)  # close only one order
4762
4763            elif args.close_orders is not None:
4764                server.CloseOrders(args.close_orders)  # close list of orders
4765
4766            elif args.close_trade:
4767                if not args.ticker:
4768                    uLogger.error("`--ticker` key is required for this operation!")
4769                    raise Exception("Ticker required")
4770
4771                server.CloseTrades([args.ticker])  # close only one trade
4772
4773            elif args.close_trades is not None:
4774                server.CloseTrades(args.close_trades)  # close trades for list of tickers
4775
4776            elif args.close_all is not None:
4777                server.CloseAll(*args.close_all)
4778
4779            elif args.limits:
4780                if args.output is not None:
4781                    server.withdrawalLimitsFile = args.output
4782
4783                server.OverviewLimits(show=True)
4784
4785            elif args.user_info:
4786                if args.output is not None:
4787                    server.userInfoFile = args.output
4788
4789                server.OverviewUserInfo(show=True)
4790
4791            elif args.account:
4792                if args.output is not None:
4793                    server.userAccountsFile = args.output
4794
4795                server.OverviewAccounts(show=True)
4796
4797            else:
4798                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
4799                raise Exception("There is no command to execute")
4800
4801    except Exception:
4802        trace = tb.format_exc()
4803        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
4804            if e in trace:
4805                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
4806                break
4807
4808        uLogger.debug(trace)
4809        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
4810        exitCode = 255  # an error occurred, must be open a ticket for this issue
4811
4812    finally:
4813        finish = datetime.now(tzutc())
4814
4815        if exitCode == 0:
4816            uLogger.debug("All operations were finished success (summary code is 0).")
4817
4818        else:
4819            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
4820                os.path.abspath(uLog.defaultLogFile), exitCode,
4821            ))
4822
4823        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
4824        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
4825            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4826            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4827        ))
4828
4829        if not kwargs:
4830            sys.exit(exitCode)
4831
4832        else:
4833            return exitCode
4834
4835
4836if __name__ == "__main__":
4837    Main()
def NanoToFloat(units: str, nano: int) -> float:
78def NanoToFloat(units: str, nano: int) -> float:
79    """
80    Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples:
81
82    `NanoToFloat(units="2", nano=500000000) -> 2.5`
83
84    `NanoToFloat(units="0", nano=50000000) -> 0.05`
85
86    :param units: integer string or integer parameter that represents the integer part of number
87    :param nano: integer string or integer parameter that represents the fractional part of number
88    :return: float view of number
89    """
90    return int(units) + int(nano) * NANO

Convert number in nano-view mode with string parameter units and integer parameter nano to float view. Examples:

NanoToFloat(units="2", nano=500000000) -> 2.5

NanoToFloat(units="0", nano=50000000) -> 0.05

Parameters
  • units: integer string or integer parameter that represents the integer part of number
  • nano: integer string or integer parameter that represents the fractional part of number
Returns

float view of number

def FloatToNano(number: float) -> dict:
 93def FloatToNano(number: float) -> dict:
 94    """
 95    Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples:
 96
 97    `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}`
 98
 99    `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}`
100
101    :param number: float number
102    :return: nano-type view of number: `{"units": "string", "nano": integer}`
103    """
104    splitByPoint = str(number).split(".")
105    frac = 0
106
107    if len(splitByPoint) > 1:
108        if len(splitByPoint[1]) <= 9:
109            frac = int("{}{}".format(
110                int(splitByPoint[1]),
111                "0" * (9 - len(splitByPoint[1])),
112            ))
113
114    if (number < 0) and (frac > 0):
115        frac = -frac
116
117    return {"units": str(int(number)), "nano": frac}

Convert float number to nano-type view: dictionary with string units and integer nano parameters {"units": "string", "nano": integer}. Examples:

FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}

FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}

Parameters
  • number: float number
Returns

nano-type view of number: {"units": "string", "nano": integer}

def GetDatesAsString(start: str = None, end: str = None) -> tuple:
120def GetDatesAsString(start: str = None, end: str = None) -> tuple:
121    """
122    Create tuple of date and time strings with timezone parsed from user-friendly date.
123
124    User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020).
125
126    Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")
127    An error exception will occur if input date has incorrect format.
128
129    If `start=None`, `end=None` then return dates from yesterday to the end of the day.
130    If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day.
131    If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`.
132    Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago.
133
134    Also, you can use keywords for start if `end=None`:
135    `today` (from 00:00:00 to the end of current day),
136    `yesterday` (-1 day from 00:00:00 to 23:59:59),
137    `week` (-7 day from 00:00:00 to the end of current day),
138    `month` (-30 day from 00:00:00 to the end of current day),
139    `year` (-365 day from 00:00:00 to the end of current day),
140
141    :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI.
142             See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`.
143             Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day.
144    """
145    uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end))
146    s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0)  # start of the current day
147    e = s.replace(hour=23, minute=59, second=59, microsecond=0)  # end of the current day
148
149    # time between start and the end of the current day:
150    if start is None or start.lower() == "today":
151        pass
152
153    # from start of the last day to the end of the last day:
154    elif start.lower() == "yesterday":
155        s -= timedelta(days=1)
156        e -= timedelta(days=1)
157
158    # week (-7 day from 00:00:00 to the end of the current day):
159    elif start.lower() == "week":
160        s -= timedelta(days=6)  # +1 current day already taken into account
161
162    # month (-30 day from 00:00:00 to the end of current day):
163    elif start.lower() == "month":
164        s -= timedelta(days=29)  # +1 current day already taken into account
165
166    # year (-365 day from 00:00:00 to the end of current day):
167    elif start.lower() == "year":
168        s -= timedelta(days=364)  # +1 current day already taken into account
169
170    # -N days ago to the end of current day:
171    elif start.startswith('-') and start[1:].isdigit():
172        s -= timedelta(days=abs(int(start)) - 1)  # +1 current day already taken into account
173
174    # dates between start day at 00:00:00 and the end of the last day at 23:59:59:
175    else:
176        s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc())
177        e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e
178
179    # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API:
180    s = s.strftime(TKS_DATE_TIME_FORMAT)
181    e = e.strftime(TKS_DATE_TIME_FORMAT)
182
183    uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e))
184
185    return s, e

Create tuple of date and time strings with timezone parsed from user-friendly date.

User dates format must be like: %Y-%m-%d, e.g. 2020-02-03 (3 Feb, 2020).

Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") An error exception will occur if input date has incorrect format.

If start=None, end=None then return dates from yesterday to the end of the day. If start=some_date_1, end=None then return dates from some_date_1 to the end of the day. If start=some_date_1, end=some_date_2 then return dates from start of some_date_1 to end of some_date_2. Start day may be negative integer numbers: -1, -2, -3 - how many days ago.

Also, you can use keywords for start if end=None: today (from 00:00:00 to the end of current day), yesterday (-1 day from 00:00:00 to 23:59:59), week (-7 day from 00:00:00 to the end of current day), month (-30 day from 00:00:00 to the end of current day), year (-365 day from 00:00:00 to the end of current day),

Returns

tuple with 2 strings (start, end) dates in UTC ISO time format %Y-%m-%dT%H:%M:%SZ for OpenAPI. See date and time format here: TKSEnums.TKS_DATE_TIME_FORMAT. Example: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z"). Second string is the end of the last day.

class TinkoffBrokerServer:
 188class TinkoffBrokerServer:
 189    """
 190    This class implements methods to work with Tinkoff broker server.
 191
 192    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
 193
 194    About `token`: https://tinkoff.github.io/investAPI/token/
 195    """
 196    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 197        """
 198        Main class init.
 199
 200        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 201        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 202                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 203        :param useCache: use default cache file with raw data to use instead of `iList`.
 204                         True by default. Cache is auto-update if new day has come.
 205                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 206        :param defaultCache: path to default cache file. `dump.json` by default.
 207        """
 208        if token is None or not token:
 209            try:
 210                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 211                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 212
 213            except KeyError:
 214                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 215                raise Exception("Token required")
 216
 217        else:
 218            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 219            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 220
 221        if accountId is None or not accountId:
 222            try:
 223                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 224                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 225
 226            except KeyError:
 227                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 228
 229        else:
 230            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 231            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 232
 233        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 234        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 235
 236        Latest version: https://pypi.org/project/tksbrokerapi/
 237        """
 238
 239        self.aliases = TKS_TICKER_ALIASES
 240        """Some aliases instead official tickers.
 241
 242        See also: `TKSEnums.TKS_TICKER_ALIASES`
 243        """
 244
 245        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 246
 247        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 248
 249        self.ticker = ""
 250        """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 251
 252        See also: `SearchByTicker()`, `SearchInstruments()`.
 253        """
 254
 255        self.figi = ""
 256        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`.
 257
 258        See also: `SearchByFIGI()`, `SearchInstruments()`.
 259        """
 260
 261        self.depth = 1
 262        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 263
 264        See also: `GetCurrentPrices()`.
 265        """
 266
 267        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 268        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 269
 270        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 271        """
 272
 273        uLogger.debug("Broker API server: {}".format(self.server))
 274
 275        self.timeout = 15
 276        """Server operations timeout in seconds. Default: `15`.
 277
 278        See also: `SendAPIRequest()`.
 279        """
 280
 281        self.headers = {
 282            "Content-Type": "application/json",
 283            "accept": "application/json",
 284            "Authorization": "Bearer {}".format(self.token),
 285            "x-app-name": "Tim55667757.TKSBrokerAPI",
 286        }
 287        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 288
 289        See also: `SendAPIRequest()`.
 290        """
 291
 292        self.body = None
 293        """Request body which send to broker server. Default: `None`.
 294
 295        See also: `SendAPIRequest()`.
 296        """
 297
 298        self.historyFile = None
 299        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only pandas dataframe.
 300
 301        See also: `History()`.
 302        """
 303
 304        self.htmlHistoryFile = "index.html"
 305        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 306
 307        See also: `ShowHistoryChart()`.
 308        """
 309
 310        self.instrumentsFile = "instruments.md"
 311        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 312
 313        See also: `ShowInstrumentsInfo()`.
 314        """
 315
 316        self.searchResultsFile = "search-results.md"
 317        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 318
 319        See also: `SearchInstruments()`.
 320        """
 321
 322        self.pricesFile = "prices.md"
 323        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 324
 325        See also: `GetListOfPrices()`.
 326        """
 327
 328        self.infoFile = "info.md"
 329        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 330
 331        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 332        """
 333
 334        self.bondsXLSXFile = "ext-bonds.xlsx"
 335        """Filename where wider pandas dataframe with more information about bonds: main info, current prices, 
 336        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 337
 338        See also: `ExtendBondsData()`.
 339        """
 340
 341        self.calendarFile = "calendar.md"
 342        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 343        
 344        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 345
 346        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 347        """
 348
 349        self.overviewFile = "overview.md"
 350        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 351
 352        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 353        """
 354
 355        self.overviewDigestFile = "overview-digest.md"
 356        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 357
 358        See also: `Overview()` with parameter `details="digest"`.
 359        """
 360
 361        self.overviewPositionsFile = "overview-positions.md"
 362        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 363
 364        See also: `Overview()` with parameter `details="positions"`.
 365        """
 366
 367        self.overviewOrdersFile = "overview-orders.md"
 368        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 369
 370        See also: `Overview()` with parameter `details="orders"`.
 371        """
 372
 373        self.overviewAnalyticsFile = "overview-analytics.md"
 374        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 375
 376        See also: `Overview()` with parameter `details="analytics"`.
 377        """
 378
 379        self.reportFile = "deals.md"
 380        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 381
 382        See also: `Deals()`.
 383        """
 384
 385        self.withdrawalLimitsFile = "limits.md"
 386        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 387
 388        See also: `OverviewLimits()` and `RequestLimits()`.
 389        """
 390
 391        self.userInfoFile = "user-info.md"
 392        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 393
 394        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 395        """
 396
 397        self.userAccountsFile = "accounts.md"
 398        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 399
 400        See also: `OverviewAccounts()`, `RequestAccounts()`.
 401        """
 402
 403        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 404        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 405
 406        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 407
 408        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 409        """
 410
 411        self.iList = None  # init iList for raw instruments data
 412        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 413        
 414        See also: `Listing()`, `DumpInstruments()`.
 415        """
 416
 417        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 418        if useCache:
 419            if os.path.exists(self.iListDumpFile):
 420                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 421                curTime = datetime.now(tzutc())
 422
 423                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 424                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 425
 426                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 427
 428                else:
 429                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 430
 431                    uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile)))
 432                    uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 433
 434            else:
 435                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 436                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 437
 438        else:
 439            self.iList = self.Listing()  # request new raw instruments data from broker server
 440            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 441
 442        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 443        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 444
 445        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 446        """
 447
 448    @staticmethod
 449    def _ParseJSON(rawData="{}", debug: bool = False) -> dict:
 450        """
 451        Parse JSON from response string.
 452
 453        :param rawData: this is a string with JSON-formatted text.
 454        :param debug: if `True` then print more debug information.
 455        :return: JSON (dictionary), parsed from server response string.
 456        """
 457        if debug:
 458            uLogger.debug("Raw text body:")
 459            uLogger.debug(rawData)
 460
 461        responseJSON = json.loads(rawData) if rawData else {}
 462
 463        if debug:
 464            uLogger.debug("JSON formatted:")
 465            for jsonLine in json.dumps(responseJSON, indent=4).split('\n'):
 466                uLogger.debug(jsonLine)
 467
 468        return responseJSON
 469
 470    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict:
 471        """
 472        Send GET or POST request to broker server and receive JSON object.
 473
 474        self.header: must be defining with dictionary of headers.
 475        self.body: if define then used as request body. None by default.
 476        self.timeout: global request timeout, 15 seconds by default.
 477        :param url: url with REST request.
 478        :param reqType: send "GET" or "POST" request. "GET" by default.
 479        :param retry: how many times retry after first request if an 5xx server errors occurred.
 480        :param pause: sleep time in seconds between retries.
 481        :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc.
 482        :return: response JSON (dictionary) from broker.
 483        """
 484        if reqType not in ("GET", "POST"):
 485            uLogger.error("You can define request type: 'GET' or 'POST'!")
 486            raise Exception("Incorrect value")
 487
 488        if debug:
 489            uLogger.debug("Request parameters:")
 490            uLogger.debug("    - REST API URL: {}".format(url))
 491            uLogger.debug("    - request type: {}".format(reqType))
 492            uLogger.debug("    - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***")))
 493            uLogger.debug("    - body: {}".format(self.body))
 494
 495        # fast hack to avoid all operations with some tickers/FIGI
 496        responseJSON = {}
 497        oK = True
 498        for item in self.exclude:
 499            if item in url:
 500                if debug:
 501                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 502
 503                oK = False
 504                break
 505
 506        if oK:
 507            counter = 0
 508            response = None
 509            errMsg = ""
 510
 511            while not response and counter <= retry:
 512                if reqType == "GET":
 513                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 514
 515                if reqType == "POST":
 516                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 517
 518                if debug:
 519                    uLogger.debug("Response:")
 520                    uLogger.debug("    - status code: {}".format(response.status_code))
 521                    uLogger.debug("    - reason: {}".format(response.reason))
 522                    uLogger.debug("    - body length: {}".format(len(response.text)))
 523                    uLogger.debug("    - headers: {}".format(response.headers))
 524
 525                # Server returns some headers:
 526                # - `x-ratelimit-limit` - shows the settings of the current user limit for this method.
 527                # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute.
 528                # - `x-ratelimit-reset` - time in seconds before resetting the request counter.
 529                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 530                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 531                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 532                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 533                    sleep(rateLimitWait)
 534
 535                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 536                if 400 <= response.status_code < 500:
 537                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 538                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 539                    counter = retry + 1
 540
 541                if 500 <= response.status_code < 600:
 542                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 543                    uLogger.debug("    - not oK, {}".format(errMsg))
 544                    counter += 1
 545
 546                    if counter <= retry:
 547                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 548                        sleep(pause)
 549
 550            responseJSON = self._ParseJSON(response.text)
 551
 552            if errMsg:
 553                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 554                uLogger.error("    - not oK, {}".format(errMsg))
 555
 556        return responseJSON
 557
 558    def _IUpdater(self, iType: str) -> tuple:
 559        """
 560        Request instrument by type from server. See available API methods for instruments:
 561        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 562        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 563        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 564        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 565        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 566
 567        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 568        :return: tuple with iType name and list of available instruments of current type for defined user token.
 569        """
 570        result = []
 571
 572        if iType in TKS_INSTRUMENTS:
 573            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 574
 575            # all instruments have the same body in API v2 requests:
 576            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 577            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 578            result = self.SendAPIRequest(instrumentURL, reqType="POST", debug=False)["instruments"]
 579
 580        return iType, result
 581
 582    def _IWrapper(self, kwargs):
 583        """
 584        Wrapper runs instrument's update method `_IUpdater()`.
 585        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 586        """
 587        return self._IUpdater(**kwargs)
 588
 589    def Listing(self) -> dict:
 590        """
 591        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 592
 593        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 594        """
 595        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 596        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 597
 598        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 599        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 600        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 601
 602        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 603        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 604        poolUpdater.close()
 605
 606        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 607        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 608        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 609
 610        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 611        for iType in iList.keys():
 612            for ticker in iList[iType]:
 613                iList[iType][ticker]["type"] = iType
 614
 615                if "minPriceIncrement" in iList[iType][ticker].keys():
 616                    iList[iType][ticker]["step"] = NanoToFloat(
 617                        iList[iType][ticker]["minPriceIncrement"]["units"],
 618                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 619                    )
 620
 621                else:
 622                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 623
 624        return iList
 625
 626    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 627        """
 628        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 629
 630        See also: `DumpInstruments()`, `Listing()`.
 631
 632        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 633                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 634        """
 635        if self.iListDumpFile is None or not self.iListDumpFile:
 636            uLogger.error("Output name of dump file must be defined!")
 637            raise Exception("Filename required")
 638
 639        if not self.iList or forceUpdate:
 640            self.iList = self.Listing()
 641
 642        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 643
 644        # Save as XLSX with separated sheets for every type of instruments:
 645        with pd.ExcelWriter(
 646                path=xlsxDumpFile,
 647                date_format=TKS_DATE_FORMAT,
 648                datetime_format=TKS_DATE_TIME_FORMAT,
 649                mode="w",
 650        ) as writer:
 651            for iType in TKS_INSTRUMENTS:
 652                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 653                df = df[sorted(df)]  # sorted by column names
 654                df = df.applymap(
 655                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 656                    na_action="ignore",
 657                )  # converting numbers from nano-type to float in every cell
 658                df.to_excel(
 659                    writer,
 660                    sheet_name=iType,
 661                    encoding="UTF-8",
 662                    freeze_panes=(1, 1),
 663                )  # saving as XLSX-file with freeze first row and column as headers
 664
 665        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 666
 667    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 668        """
 669        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 670        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 671
 672        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 673
 674        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 675                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 676        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 677        """
 678        if self.iListDumpFile is None or not self.iListDumpFile:
 679            uLogger.error("Output name of dump file must be defined!")
 680            raise Exception("Filename required")
 681
 682        if not self.iList or forceUpdate:
 683            self.iList = self.Listing()
 684
 685        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 686        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 687            fH.write(jsonDump)
 688
 689        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 690
 691        return jsonDump
 692
 693    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 694        """
 695        Show information about one instrument defined by json data and prints it in Markdown format.
 696
 697        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 698
 699        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
 700        :param show: if `True` then also printing information about instrument and its current price.
 701        :return: multilines text in Markdown format with information about one instrument.
 702        """
 703        splitLine = "|                                                             |                                                        |\n"
 704        infoText = ""
 705
 706        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 707            info = [
 708                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
 709                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
 710                "| Parameters                                                  | Values                                                 |\n",
 711                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 712                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 713                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 714            ]
 715
 716            if "sector" in iJSON.keys() and iJSON["sector"]:
 717                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 718
 719            info.append("| Country of instrument:                                      | {:<54} |\n".format("{}{}".format(
 720                "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "",
 721                iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "",
 722            )))
 723
 724            info.extend([
 725                splitLine,
 726                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 727                "| Exchange:                                                   | {:<54} |\n".format(iJSON["exchange"]),
 728            ])
 729
 730            if "isin" in iJSON.keys() and iJSON["isin"]:
 731                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 732
 733            if "classCode" in iJSON.keys():
 734                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 735
 736            info.extend([
 737                splitLine,
 738                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 739                splitLine,
 740                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 741                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 742                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 743            ])
 744
 745            if iJSON["figi"]:
 746                self.figi = iJSON["figi"]
 747                iJSON = iJSON | self.RequestTradingStatus()
 748
 749                info.extend([
 750                    splitLine,
 751                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 752                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 753                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 754                ])
 755
 756            info.append(splitLine)
 757
 758            if "type" in iJSON.keys() and iJSON["type"]:
 759                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 760
 761            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 762                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 763
 764            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 765                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 766
 767            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 768                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 769
 770            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 771                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 772
 773            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 774                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 775
 776            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 777                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 778
 779            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 780                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 781
 782            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 783                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 784
 785            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 786                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 787
 788            if "currency" in iJSON.keys():
 789                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 790
 791            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 792                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 793
 794            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 795                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 796
 797            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 798                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 799
 800            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 801                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 802
 803            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 804                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 805
 806            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 807                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 808
 809            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 810                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 811
 812            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 813                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 814
 815            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 816                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 817
 818            iExt = None
 819            if iJSON["type"] == "Bonds":
 820                info.extend([
 821                    splitLine,
 822                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 823                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 824                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 825                        iJSON["nominal"]["currency"],
 826                    )),
 827                ])
 828
 829                if "floatingCouponFlag" in iJSON.keys():
 830                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 831
 832                if "amortizationFlag" in iJSON.keys():
 833                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 834
 835                info.append(splitLine)
 836
 837                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 838                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 839
 840                iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 841
 842                info.extend([
 843                    "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 844                    "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 845                    "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 846                ])
 847
 848                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 849                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 850                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 851                        iJSON["aciValue"]["currency"]
 852                    )))
 853
 854            if "currentPrice" in iJSON.keys():
 855                info.append(splitLine)
 856
 857                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 858                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 859
 860                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 861                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 862                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 863                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 864                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 865
 866                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 867                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 868
 869                info.extend([
 870                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 871                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 872                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 873                    )),
 874                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 875                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 876                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 877                    )),
 878                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 879                        "{:.2f}%{}".format(
 880                            iJSON["currentPrice"]["changes"],
 881                            " ({}{:.2f} {})".format(
 882                                "+" if bondChangesDelta > 0 else "",
 883                                bondChangesDelta,
 884                                aciCurrency
 885                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 886                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 887                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 888                                currency
 889                            ),
 890                        )
 891                    ),
 892                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 893                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 894                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 895                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 896                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 897                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 898                    )),
 899                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 900                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 901                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 902                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 903                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 904                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 905                    )),
 906                ])
 907
 908            if "lot" in iJSON.keys():
 909                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 910
 911            if "step" in iJSON.keys() and iJSON["step"] != 0:
 912                info.append("| Minimum price increment (step):                             | {:<54} |\n".format(iJSON["step"]))
 913
 914            # Add bond payment calendar:
 915            if iJSON["type"] == "Bonds":
 916                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 917                info.extend(["\n", strCalendar])
 918
 919            infoText += "".join(info)
 920
 921            if show:
 922                uLogger.info("{}".format(infoText))
 923
 924            else:
 925                uLogger.debug("{}".format(infoText))
 926
 927            if self.infoFile is not None:
 928                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 929                    fH.write(infoText)
 930
 931                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 932
 933        return infoText
 934
 935    def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
 936        """
 937        Search and return raw broker's information about instrument by its ticker.
 938        `ticker` must be defined! If debug=True then print all debug messages.
 939
 940        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 941        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 942        :param debug: if `True` then print all debug console messages.
 943        :return: JSON formatted data with information about instrument.
 944        """
 945        tickerJSON = {}
 946        if debug:
 947            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 948
 949        if not self.ticker:
 950            uLogger.warning("self.ticker variable is not be empty!")
 951
 952        else:
 953            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 954                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 955                raise Exception("Instrument not allowed")
 956
 957            if not self.iList:
 958                self.iList = self.Listing()
 959
 960            if self.ticker in self.iList["Shares"].keys():
 961                tickerJSON = self.iList["Shares"][self.ticker]
 962                if debug:
 963                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 964
 965            elif self.ticker in self.iList["Currencies"].keys():
 966                tickerJSON = self.iList["Currencies"][self.ticker]
 967                if debug:
 968                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 969
 970            elif self.ticker in self.iList["Bonds"].keys():
 971                tickerJSON = self.iList["Bonds"][self.ticker]
 972                if debug:
 973                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 974
 975            elif self.ticker in self.iList["Etfs"].keys():
 976                tickerJSON = self.iList["Etfs"][self.ticker]
 977                if debug:
 978                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 979
 980            elif self.ticker in self.iList["Futures"].keys():
 981                tickerJSON = self.iList["Futures"][self.ticker]
 982                if debug:
 983                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 984
 985        if tickerJSON:
 986            self.figi = tickerJSON["figi"]
 987
 988            if requestPrice:
 989                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 990
 991                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 992                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 993
 994                else:
 995                    tickerJSON["currentPrice"]["changes"] = 0
 996
 997            if show:
 998                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 999
1000        else:
1001            if show:
1002                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
1003
1004        return tickerJSON
1005
1006    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
1007        """
1008        Search and return raw broker's information about instrument by its FIGI.
1009        `figi` must be defined! If debug=True then print all debug messages.
1010
1011        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1012        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1013        :param debug: if `True` then print all debug console messages.
1014        :return: JSON formatted data with information about instrument.
1015        """
1016        figiJSON = {}
1017        if debug:
1018            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
1019
1020        if not self.figi:
1021            uLogger.warning("self.figi variable is not be empty!")
1022
1023        else:
1024            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1025                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
1026                raise Exception("Instrument not allowed")
1027
1028            if not self.iList:
1029                self.iList = self.Listing()
1030
1031            for item in self.iList["Shares"].keys():
1032                if self.figi == self.iList["Shares"][item]["figi"]:
1033                    figiJSON = self.iList["Shares"][item]
1034
1035                    if debug:
1036                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
1037
1038                    break
1039
1040            if not figiJSON:
1041                for item in self.iList["Currencies"].keys():
1042                    if self.figi == self.iList["Currencies"][item]["figi"]:
1043                        figiJSON = self.iList["Currencies"][item]
1044
1045                        if debug:
1046                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1047
1048                        break
1049
1050            if not figiJSON:
1051                for item in self.iList["Bonds"].keys():
1052                    if self.figi == self.iList["Bonds"][item]["figi"]:
1053                        figiJSON = self.iList["Bonds"][item]
1054
1055                        if debug:
1056                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1057
1058                        break
1059
1060            if not figiJSON:
1061                for item in self.iList["Etfs"].keys():
1062                    if self.figi == self.iList["Etfs"][item]["figi"]:
1063                        figiJSON = self.iList["Etfs"][item]
1064
1065                        if debug:
1066                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1067
1068                        break
1069
1070            if not figiJSON:
1071                for item in self.iList["Futures"].keys():
1072                    if self.figi == self.iList["Futures"][item]["figi"]:
1073                        figiJSON = self.iList["Futures"][item]
1074
1075                        if debug:
1076                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1077
1078                        break
1079
1080        if figiJSON:
1081            self.figi = figiJSON["figi"]
1082            self.ticker = figiJSON["ticker"]
1083
1084            if requestPrice:
1085                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1086
1087                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1088                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1089
1090                else:
1091                    figiJSON["currentPrice"]["changes"] = 0
1092
1093            if show:
1094                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1095
1096        else:
1097            if show:
1098                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1099
1100        return figiJSON
1101
1102    def GetCurrentPrices(self, show: bool = True) -> dict:
1103        """
1104        Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record:
1105        `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1106
1107        See also:
1108
1109        :param show: if `True` then print DOM to log and console.
1110        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1111        """
1112        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1113
1114        if self.depth < 1:
1115            uLogger.error("Depth of Market (DOM) must be >=1!")
1116            raise Exception("Incorrect value")
1117
1118        if not (self.ticker or self.figi):
1119            uLogger.error("self.ticker or self.figi variables must be defined!")
1120            raise Exception("Ticker or FIGI required")
1121
1122        if self.ticker and not self.figi:
1123            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1124            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1125
1126        if not self.ticker and self.figi:
1127            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1128            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1129
1130        if not self.figi:
1131            uLogger.error("FIGI is not defined!")
1132            raise Exception("Ticker or FIGI required")
1133
1134        else:
1135            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1136
1137            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1138            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1139            self.body = str({"figi": self.figi, "depth": self.depth})
1140            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")
1141
1142            if pricesResponse:
1143                # list of dicts with sellers orders:
1144                prices["buy"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1145
1146                # list of dicts with buyers orders:
1147                prices["sell"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1148
1149                # max price of instrument at this time:
1150                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1151
1152                # min price of instrument at this time:
1153                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1154
1155                # last price of deal with instrument:
1156                prices["lastPrice"] = NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]) if "lastPrice" in pricesResponse.keys() else 0
1157
1158                # last close price of instrument:
1159                prices["closePrice"] = NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]) if "closePrice" in pricesResponse.keys() else 0
1160
1161            else:
1162                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1163                uLogger.debug("Server response: {}".format(pricesResponse))
1164
1165            if show:
1166                if prices["buy"] or prices["sell"]:
1167                    info = [
1168                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1169                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1170                            self.ticker,
1171                            self.figi,
1172                            self.depth,
1173                        ),
1174                        uLog.sepShort, "\n",
1175                        " Orders of Buyers   | Orders of Sellers\n",
1176                        uLog.sepShort, "\n",
1177                        " Sell prices (vol.) | Buy prices (vol.)\n",
1178                        uLog.sepShort, "\n",
1179                    ]
1180
1181                    if not prices["buy"]:
1182                        info.append("                    | No orders!\n")
1183                        sumBuy = 0
1184
1185                    else:
1186                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1187                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1188                        for item in maxMinSorted:
1189                            info.append("                    | {} ({})\n".format(item["price"], item["quantity"]))
1190
1191                    if not prices["sell"]:
1192                        info.append("No orders!          |\n")
1193                        sumSell = 0
1194
1195                    else:
1196                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1197                        for item in prices["sell"]:
1198                            info.append("{:>19} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1199
1200                    info.extend([
1201                        uLog.sepShort, "\n",
1202                        "{:>19} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1203                        uLog.sepShort, "\n",
1204                    ])
1205
1206                    infoText = "".join(info)
1207
1208                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1209
1210                else:
1211                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1212
1213        return prices
1214
1215    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1216        """
1217        This method get and show information about all available broker instruments for current user account.
1218        If `instrumentsFile` string is not empty then also save information to this file.
1219
1220        :param show: if `True` then print results to console, if `False` - print only to file.
1221        :return: multi-lines string with all available broker instruments
1222        """
1223        if not self.iList:
1224            self.iList = self.Listing()
1225
1226        info = [
1227            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1228            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1229        ]
1230
1231        # add instruments count by type:
1232        for iType in self.iList.keys():
1233            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1234
1235        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1236        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1237
1238        # generating info tables with all instruments by type:
1239        for iType in self.iList.keys():
1240            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1241
1242            for instrument in self.iList[iType].keys():
1243                iName = self.iList[iType][instrument]["name"]  # instrument's name
1244                if len(iName) > 57:
1245                    iName = "{}...".format(iName[:54])  # right trim for a long string
1246
1247                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1248                    self.iList[iType][instrument]["ticker"],
1249                    iName,
1250                    self.iList[iType][instrument]["figi"],
1251                    self.iList[iType][instrument]["currency"],
1252                    self.iList[iType][instrument]["lot"],
1253                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1254                ))
1255
1256        infoText = "".join(info)
1257
1258        if show:
1259            uLogger.info(infoText)
1260
1261        if self.instrumentsFile:
1262            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1263                fH.write(infoText)
1264
1265            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1266
1267        return infoText
1268
1269    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1270        """
1271        This method search and show information about instruments by part of its ticker, FIGI or name.
1272        If `searchResultsFile` string is not empty then also save information to this file.
1273
1274        :param pattern: string with part of ticker, FIGI or instrument's name.
1275        :param show: if `True` then print results to console, if `False` - return list of result only.
1276        :return: list of dictionaries with all found instruments.
1277        """
1278        if not self.iList:
1279            self.iList = self.Listing()
1280
1281        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1282        compiledPattern = re.compile(pattern, re.IGNORECASE)
1283
1284        for iType in self.iList:
1285            for instrument in self.iList[iType].values():
1286                searchResult = compiledPattern.search(" ".join(
1287                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1288                ))
1289
1290                if searchResult:
1291                    searchResults[iType][instrument["ticker"]] = instrument
1292
1293        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1294        info = [
1295            "# Search results\n\n",
1296            "* **Search pattern:** [{}]\n".format(pattern),
1297            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1298            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1299        ]
1300        infoShort = info[:]
1301
1302        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1303        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1304        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1305
1306        if resultsLen == 0:
1307            info.append("\nNo results\n")
1308            infoShort.append("\nNo results\n")
1309            uLogger.warning("No results. Try changing your search pattern.")
1310
1311        else:
1312            for iType in searchResults:
1313                iTypeValuesCount = len(searchResults[iType].values())
1314                if iTypeValuesCount > 0:
1315                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1316                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1317
1318                    for instrument in searchResults[iType].values():
1319                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1320                            instrument["type"],
1321                            instrument["ticker"],
1322                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1323                            instrument["figi"],
1324                        ))
1325
1326                    if iTypeValuesCount <= 5:
1327                        infoShort.extend(info[-iTypeValuesCount:])
1328
1329                    else:
1330                        infoShort.extend(info[-5:])
1331                        infoShort.append(skippedLine)
1332
1333        infoText = "".join(info)
1334        infoTextShort = "".join(infoShort)
1335
1336        if show:
1337            uLogger.info(infoTextShort)
1338            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1339
1340        if self.searchResultsFile:
1341            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1342                fH.write(infoText)
1343
1344            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1345
1346        return searchResults
1347
1348    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1349        """
1350        Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
1351
1352        :param instruments: list of strings with tickers or FIGIs.
1353        :return: list with unique instrument FIGIs only.
1354        """
1355        requestedInstruments = []
1356        for iName in instruments:
1357            if iName not in self.aliases.keys():
1358                if iName not in requestedInstruments:
1359                    requestedInstruments.append(iName)
1360
1361            else:
1362                if iName not in requestedInstruments:
1363                    if self.aliases[iName] not in requestedInstruments:
1364                        requestedInstruments.append(self.aliases[iName])
1365
1366        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1367
1368        onlyUniqueFIGIs = []
1369        for iName in requestedInstruments:
1370            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1371                continue
1372
1373            self.ticker = iName
1374            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1375
1376            if not iData:
1377                self.ticker = ""
1378                self.figi = iName
1379
1380                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1381
1382                if not iData:
1383                    self.figi = ""
1384                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1385
1386            if iData and iData["figi"] not in onlyUniqueFIGIs:
1387                onlyUniqueFIGIs.append(iData["figi"])
1388
1389        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1390
1391        return onlyUniqueFIGIs
1392
1393    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1394        """
1395        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1396        See limits: https://tinkoff.github.io/investAPI/limits/
1397        If `pricesFile` string is not empty then also save information to this file.
1398
1399        :param instruments: list of strings with tickers or FIGIs.
1400        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1401        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1402                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1403        """
1404        if instruments is None or not instruments:
1405            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1406            raise Exception("Ticker or FIGI required")
1407
1408        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1409
1410        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1411
1412        iList = []  # trying to get info and current prices about all unique instruments:
1413        for self.figi in onlyUniqueFIGIs:
1414            iData = self.SearchByFIGI(requestPrice=True)
1415            iList.append(iData)
1416
1417        self.ShowListOfPrices(iList, show)
1418
1419        return iList
1420
1421    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1422        """
1423        Show table contains current prices of given instruments.
1424
1425        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1426                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1427        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1428        :return: multilines text in Markdown format as a table contains current prices.
1429        """
1430        infoText = ""
1431
1432        if show or self.pricesFile:
1433            info = [
1434                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1435                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1436                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1437            ]
1438
1439            for item in iList:
1440                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1441                    item["ticker"],
1442                    item["figi"],
1443                    item["type"],
1444                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1445                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1446                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1447                    "{} / {}".format(
1448                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1449                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1450                    ),
1451                    "{} / {}".format(
1452                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1453                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1454                    ),
1455                    item["currency"],
1456                ))
1457
1458            infoText = "".join(info)
1459
1460            if show:
1461                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1462
1463            if self.pricesFile:
1464                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1465                    fH.write(infoText)
1466
1467                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1468
1469        return infoText
1470
1471    def RequestTradingStatus(self) -> dict:
1472        """
1473        Requesting trading status for the instrument defined by `figi` variable.
1474        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1475        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1476
1477        :return: dictionary with trading status attributes. Response example:
1478                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1479                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1480        """
1481        if self.figi is None or not self.figi:
1482            uLogger.error("Variable `figi` must be defined for using this method!")
1483            raise Exception("FIGI required")
1484
1485        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1486
1487        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1488        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1489        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1490
1491        uLogger.debug("Records about current trading status successfully received")
1492
1493        return tradingStatus
1494
1495    def RequestPortfolio(self) -> dict:
1496        """
1497        Requesting actual user's portfolio for current `accountId`.
1498        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1499        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1500
1501        :return: dictionary with user's portfolio.
1502        """
1503        if self.accountId is None or not self.accountId:
1504            uLogger.error("Variable `accountId` must be defined for using this method!")
1505            raise Exception("Account ID required")
1506
1507        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1508
1509        self.body = str({"accountId": self.accountId})
1510        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1511        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1512
1513        uLogger.debug("Records about user's portfolio successfully received")
1514
1515        return rawPortfolio
1516
1517    def RequestPositions(self) -> dict:
1518        """
1519        Requesting open positions by currencies and instruments for current `accountId`.
1520        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1521        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1522
1523        :return: dictionary with open positions by instruments.
1524        """
1525        if self.accountId is None or not self.accountId:
1526            uLogger.error("Variable `accountId` must be defined for using this method!")
1527            raise Exception("Account ID required")
1528
1529        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1530
1531        self.body = str({"accountId": self.accountId})
1532        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1533        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1534
1535        uLogger.debug("Records about current open positions successfully received")
1536
1537        return rawPositions
1538
1539    def RequestPendingOrders(self) -> list:
1540        """
1541        Requesting current actual pending orders for current `accountId`.
1542        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1543        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1544
1545        :return: list of dictionaries with pending orders.
1546        """
1547        if self.accountId is None or not self.accountId:
1548            uLogger.error("Variable `accountId` must be defined for using this method!")
1549            raise Exception("Account ID required")
1550
1551        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1552
1553        self.body = str({"accountId": self.accountId})
1554        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1555        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1556
1557        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1558
1559        return rawOrders
1560
1561    def RequestStopOrders(self) -> list:
1562        """
1563        Requesting current actual stop orders for current `accountId`.
1564        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1565        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1566
1567        :return: list of dictionaries with stop orders.
1568        """
1569        if self.accountId is None or not self.accountId:
1570            uLogger.error("Variable `accountId` must be defined for using this method!")
1571            raise Exception("Account ID required")
1572
1573        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1574
1575        self.body = str({"accountId": self.accountId})
1576        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1577        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1578
1579        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1580
1581        return rawStopOrders
1582
1583    def Overview(self, show: bool = False, details: str = "full") -> dict:
1584        """
1585        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1586        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1587        are defined then also save information to file.
1588
1589        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1590        many requests about the state of the portfolio, and then, based on the received data, a large number
1591        of calculation and statistics are collected.
1592
1593        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1594        :param details: how detailed should the information be? You should specify one of strings:
1595                        `full` - shows full available information about portfolio status (by default),
1596                        `positions` - shows only open positions,
1597                        `digest` - show a short digest of the portfolio status,
1598                        `analytics` - shows only the analytics section and the distribution of the portfolio by various categories,
1599                        `orders` - shows only sections of open limits and stop orders.
1600        :return: dictionary with client's raw portfolio and some statistics.
1601        """
1602        if self.accountId is None or not self.accountId:
1603            uLogger.error("Variable `accountId` must be defined for using this method!")
1604            raise Exception("Account ID required")
1605
1606        view = {
1607            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1608                "headers": {},  # list of dictionaries, response headers without "positions" section
1609                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1610                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1611                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1612                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1613                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1614                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1615                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1616                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1617                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1618            },
1619            "stat": {  # --- some statistics calculated using "raw" sections:
1620                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1621                "availableRUB": 0.,  # available rubles (without other currencies)
1622                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1623                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1624                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1625                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1626                "sharesCostRUB": 0.,  # costs of all shares in RUB
1627                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1628                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1629                "futuresCostRUB": 0.,  # costs of all futures in RUB
1630                "Currencies": [],  # list of dictionaries of all currencies statistics
1631                "Shares": [],  # list of dictionaries of all shares statistics
1632                "Bonds": [],  # list of dictionaries of all bonds statistics
1633                "Etfs": [],  # list of dictionaries of all etfs statistics
1634                "Futures": [],  # list of dictionaries of all futures statistics
1635                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1636                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1637                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1638                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1639                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1640            },
1641            "analytics": {  # --- some analytics of portfolio:
1642                "distrByAssets": {},  # portfolio distribution by assets
1643                "distrByCompanies": {},  # portfolio distribution by companies
1644                "distrBySectors": {},  # portfolio distribution by sectors
1645                "distrByCurrencies": {},  # portfolio distribution by currencies
1646                "distrByCountries": {},  # portfolio distribution by countries
1647            }
1648        }
1649
1650        details = details.lower()
1651        availableDetails = ["full", "positions", "digest", "analytics", "orders"]
1652        if details not in availableDetails:
1653            details = "full"
1654            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1655
1656        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1657
1658        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1659        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1660        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1661        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1662
1663        # save response headers without "positions" section:
1664        for key in portfolioResponse.keys():
1665            if key != "positions":
1666                view["raw"]["headers"][key] = portfolioResponse[key]
1667
1668            else:
1669                continue
1670
1671        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1672        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1673        for item in portfolioResponse["positions"]:
1674            if item["instrumentType"] == "currency":
1675                self.figi = item["figi"]
1676                curr = self.SearchByFIGI(requestPrice=False)
1677
1678                # current price of currency in RUB:
1679                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1680                    "name": curr["name"],
1681                    "currentPrice": NanoToFloat(
1682                        item["currentPrice"]["units"],
1683                        item["currentPrice"]["nano"]
1684                    ),
1685                }
1686
1687                view["raw"]["Currencies"].append(item)
1688
1689            elif item["instrumentType"] == "share":
1690                view["raw"]["Shares"].append(item)
1691
1692            elif item["instrumentType"] == "bond":
1693                view["raw"]["Bonds"].append(item)
1694
1695            elif item["instrumentType"] == "etf":
1696                view["raw"]["Etfs"].append(item)
1697
1698            elif item["instrumentType"] == "futures":
1699                view["raw"]["Futures"].append(item)
1700
1701            else:
1702                continue
1703
1704        # how many volume of currencies (by ISO currency name) are blocked:
1705        for item in view["raw"]["positions"]["blocked"]:
1706            blocked = NanoToFloat(item["units"], item["nano"])
1707            if blocked > 0:
1708                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1709
1710        # how many volume of instruments (by FIGI) are blocked:
1711        for item in view["raw"]["positions"]["securities"]:
1712            blocked = int(item["blocked"])
1713            if blocked > 0:
1714                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1715
1716        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1717
1718        if "rub" in allBlocked.keys():
1719            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1720
1721        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1722        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1723        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1724        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1725        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1726        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1727        view["stat"]["portfolioCostRUB"] = sum([
1728            view["stat"]["allCurrenciesCostRUB"],
1729            view["stat"]["sharesCostRUB"],
1730            view["stat"]["bondsCostRUB"],
1731            view["stat"]["etfsCostRUB"],
1732            view["stat"]["futuresCostRUB"],
1733        ])
1734
1735        # --- calculating some portfolio statistics:
1736        byComp = {}  # distribution by companies
1737        bySect = {}  # distribution by sectors
1738        byCurr = {}  # distribution by currencies (include RUB)
1739        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1740        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1741
1742        for item in portfolioResponse["positions"]:
1743            self.figi = item["figi"]
1744            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1745
1746            if instrument:
1747                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1748                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1749
1750                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1751                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1752
1753                else:
1754                    blocked = 0
1755
1756                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1757                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1758                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1759                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1760                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1761                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1762                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1763                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1764                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1765                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1766                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1767                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1768
1769                statData = {
1770                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1771                    "ticker": instrument["ticker"],  # ticker by FIGI
1772                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1773                    "volume": volume,  # available volume of instrument
1774                    "lots": lots,  # volume in lots of instrument
1775                    "direction": direction,  # direction of an instrument's position: short or long
1776                    "blocked": blocked,  # blocked volume of currency or instrument
1777                    "currentPrice": curPrice,  # current instrument's price in basic asset
1778                    "average": average,  # current average position price
1779                    "cost": cost,  # current cost of all volume of instrument in basic asset
1780                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1781                    "costRUB": costRUB,  # cost of instrument in ruble
1782                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1783                    "profit": profit,  # expected profit at current moment
1784                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1785                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1786                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1787                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1788                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1789                    "step": instrument["step"],  # minimum price increment
1790                }
1791
1792                # adding distribution by unique countries:
1793                if statData["country"] not in byCountry.keys():
1794                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1795
1796                else:
1797                    byCountry[statData["country"]]["cost"] += costRUB
1798                    byCountry[statData["country"]]["percent"] += percentCostRUB
1799
1800                if item["instrumentType"] != "currency":
1801                    # adding distribution by unique companies:
1802                    if statData["name"]:
1803                        if statData["name"] not in byComp.keys():
1804                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1805
1806                        else:
1807                            byComp[statData["name"]]["cost"] += costRUB
1808                            byComp[statData["name"]]["percent"] += percentCostRUB
1809
1810                    # adding distribution by unique sectors:
1811                    if statData["sector"] not in bySect.keys():
1812                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1813
1814                    else:
1815                        bySect[statData["sector"]]["cost"] += costRUB
1816                        bySect[statData["sector"]]["percent"] += percentCostRUB
1817
1818                # adding distribution by unique currencies:
1819                if currency not in byCurr.keys():
1820                    byCurr[currency] = {
1821                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1822                        "cost": costRUB,
1823                        "percent": percentCostRUB
1824                    }
1825
1826                else:
1827                    byCurr[currency]["cost"] += costRUB
1828                    byCurr[currency]["percent"] += percentCostRUB
1829
1830                # saving statistics for every instrument:
1831                if item["instrumentType"] == "currency":
1832                    view["stat"]["Currencies"].append(statData)
1833
1834                    # update dict with free funds for trading (total - blocked) by currencies
1835                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1836                    view["stat"]["funds"][currency] = {
1837                        "total": volume,
1838                        "totalCostRUB": costRUB,  # total volume cost in rubles
1839                        "free": volume - blocked,
1840                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1841                    }
1842
1843                elif item["instrumentType"] == "share":
1844                    view["stat"]["Shares"].append(statData)
1845
1846                elif item["instrumentType"] == "bond":
1847                    view["stat"]["Bonds"].append(statData)
1848
1849                elif item["instrumentType"] == "etf":
1850                    view["stat"]["Etfs"].append(statData)
1851
1852                elif item["instrumentType"] == "Futures":
1853                    view["stat"]["Futures"].append(statData)
1854
1855                else:
1856                    continue
1857
1858        # total changes in Russian Ruble:
1859        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1860        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1861        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1862        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1863        view["stat"]["funds"]["rub"] = {
1864            "total": view["stat"]["availableRUB"],
1865            "totalCostRUB": view["stat"]["availableRUB"],
1866            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1867            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1868        }
1869
1870        # --- pending orders sector data:
1871        uniquePendingOrders = []
1872        uniquePendingOrdersFIGIs = []
1873        for item in view["raw"]["orders"]:
1874            if item["figi"] not in uniquePendingOrdersFIGIs:
1875                uniquePendingOrdersFIGIs.append(item["figi"])
1876                uniquePendingOrders.append(item)
1877
1878        for item in uniquePendingOrders:
1879            self.figi = item["figi"]
1880            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1881
1882            if instrument:
1883                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1884                orderType = TKS_ORDER_TYPES[item["orderType"]]
1885                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1886                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1887
1888                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1889                if item["direction"] == "ORDER_DIRECTION_BUY":
1890                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1891
1892                else:
1893                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1894
1895                # requested price for order execution:
1896                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1897
1898                # necessary changes in percent to reach target from current price:
1899                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1900
1901                view["stat"]["orders"].append({
1902                    "orderID": item["orderId"],  # orderId number parameter of current order
1903                    "figi": item["figi"],  # FIGI identification
1904                    "ticker": instrument["ticker"],  # ticker name by FIGI
1905                    "lotsRequested": item["lotsRequested"],  # requested lots value
1906                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1907                    "currentPrice": lastPrice,  # current instrument's price for defined action
1908                    "targetPrice": target,  # requested price for order execution in base currency
1909                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1910                    "percentChanges": changes,  # changes in percent to target from current price
1911                    "currency": item["currency"],  # instrument's currency name
1912                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1913                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1914                    "status": orderState,  # order status from TKS_ORDER_STATES
1915                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1916                })
1917
1918        # --- stop orders sector data:
1919        uniqueStopOrders = []
1920        uniqueStopOrdersFIGIs = []
1921        for item in view["raw"]["stopOrders"]:
1922            if item["figi"] not in uniqueStopOrdersFIGIs:
1923                uniqueStopOrdersFIGIs.append(item["figi"])
1924                uniqueStopOrders.append(item)
1925
1926        for item in uniqueStopOrders:
1927            self.figi = item["figi"]
1928            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1929
1930            if instrument:
1931                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1932                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1933                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1934
1935                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1936                if "expirationTime" in item.keys():
1937                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1938                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1939
1940                else:
1941                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1942                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1943
1944                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1945                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1946                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1947
1948                else:
1949                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1950
1951                # requested price when stop-order executed:
1952                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1953
1954                # price for limit-order, set up when stop-order executed:
1955                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1956
1957                # necessary changes in percent to reach target from current price:
1958                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1959
1960                view["stat"]["stopOrders"].append({
1961                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1962                    "figi": item["figi"],  # FIGI identification
1963                    "ticker": instrument["ticker"],  # ticker name by FIGI
1964                    "lotsRequested": item["lotsRequested"],  # requested lots value
1965                    "currentPrice": lastPrice,  # current instrument's price for defined action
1966                    "targetPrice": target,  # requested price for stop-order execution in base currency
1967                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1968                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1969                    "percentChanges": changes,  # changes in percent to target from current price
1970                    "currency": item["currency"],  # instrument's currency name
1971                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1972                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1973                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1974                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1975                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1976                })
1977
1978        # --- calculating data for analytics section:
1979        # portfolio distribution by assets:
1980        view["analytics"]["distrByAssets"] = {
1981            "Ruble": {
1982                "uniques": 1,
1983                "cost": view["stat"]["availableRUB"],
1984                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1985            },
1986            "Currencies": {
1987                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
1988                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
1989                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1990            },
1991            "Shares": {
1992                "uniques": len(view["stat"]["Shares"]),
1993                "cost": view["stat"]["sharesCostRUB"],
1994                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1995            },
1996            "Bonds": {
1997                "uniques": len(view["stat"]["Bonds"]),
1998                "cost": view["stat"]["bondsCostRUB"],
1999                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2000            },
2001            "Etfs": {
2002                "uniques": len(view["stat"]["Etfs"]),
2003                "cost": view["stat"]["etfsCostRUB"],
2004                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2005            },
2006            "Futures": {
2007                "uniques": len(view["stat"]["Futures"]),
2008                "cost": view["stat"]["futuresCostRUB"],
2009                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2010            },
2011        }
2012
2013        # portfolio distribution by companies:
2014        view["analytics"]["distrByCompanies"]["All money cash"] = {
2015            "ticker": "",
2016            "cost": view["stat"]["allCurrenciesCostRUB"],
2017            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2018        }
2019        view["analytics"]["distrByCompanies"].update(byComp)
2020
2021        # portfolio distribution by sectors:
2022        view["analytics"]["distrBySectors"]["All money cash"] = {
2023            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2024            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2025        }
2026        view["analytics"]["distrBySectors"].update(bySect)
2027
2028        # portfolio distribution by currencies:
2029        view["analytics"]["distrByCurrencies"].update(byCurr)
2030        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2031        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2032
2033        # portfolio distribution by countries:
2034        view["analytics"]["distrByCountries"].update(byCountry)
2035
2036        # --- Prepare text statistics overview in human-readable:
2037        if show:
2038            # Whatever the value `details`, header not changes:
2039            info = [
2040                "# Client's portfolio\n\n",
2041                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2042                "* **Account ID:** [{}]\n".format(self.accountId),
2043            ]
2044
2045            if details in ["full", "positions", "digest"]:
2046                info.extend([
2047                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2048                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2049                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2050                        view["stat"]["totalChangesRUB"],
2051                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2052                        view["stat"]["totalChangesPercentRUB"],
2053                    ),
2054                ])
2055
2056            if details in ["full", "positions"]:
2057                info.extend([
2058                    "## Open positions\n\n",
2059                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2060                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2061                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2062                        "{:.2f} ({:.2f}) rub".format(
2063                            view["stat"]["availableRUB"],
2064                            view["stat"]["blockedRUB"],
2065                        )
2066                    )
2067                ])
2068
2069                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2070                    return [
2071                        "|                             |                                 |          |              |              |                     |                              |\n",
2072                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2073                            noTradeStr if noTradeStr else typeStr,
2074                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2075                        ),
2076                    ]
2077
2078                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2079                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2080                        "{} [{}]".format(data["ticker"], data["figi"]),
2081                        "{:.2f} ({:.2f}) {}".format(
2082                            data["volume"],
2083                            data["blocked"],
2084                            data["currency"],
2085                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2086                            data["volume"],
2087                            data["blocked"],
2088                        ),
2089                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2090                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2091                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2092                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2093                        "{}{:.2f} {} ({}{:.2f}%)".format(
2094                            "+" if data["profit"] > 0 else "",
2095                            data["profit"], data["baseCurrencyName"],
2096                            "+" if data["percentProfit"] > 0 else "",
2097                            data["percentProfit"],
2098                        ),
2099                    )
2100
2101                # --- Show currencies section:
2102                if view["stat"]["Currencies"]:
2103                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2104                    for item in view["stat"]["Currencies"]:
2105                        info.append(_InfoStr(item, showCurrencyName=True))
2106
2107                else:
2108                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2109
2110                # --- Show shares section:
2111                if view["stat"]["Shares"]:
2112                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2113
2114                    for item in view["stat"]["Shares"]:
2115                        info.append(_InfoStr(item))
2116
2117                else:
2118                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2119
2120                # --- Show bonds section:
2121                if view["stat"]["Bonds"]:
2122                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2123
2124                    for item in view["stat"]["Bonds"]:
2125                        info.append(_InfoStr(item))
2126
2127                else:
2128                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2129
2130                # --- Show etfs section:
2131                if view["stat"]["Etfs"]:
2132                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2133
2134                    for item in view["stat"]["Etfs"]:
2135                        info.append(_InfoStr(item))
2136
2137                else:
2138                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2139
2140                # --- Show futures section:
2141                if view["stat"]["Futures"]:
2142                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2143
2144                    for item in view["stat"]["Futures"]:
2145                        info.append(_InfoStr(item))
2146
2147                else:
2148                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2149
2150            if details in ["full", "orders"]:
2151                # --- Show pending orders section:
2152                if view["stat"]["orders"]:
2153                    info.extend([
2154                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2155                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2156                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2157                    ])
2158
2159                    for item in view["stat"]["orders"]:
2160                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2161                            "{} [{}]".format(item["ticker"], item["figi"]),
2162                            item["orderID"],
2163                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2164                            "{} {} ({}{:.2f}%)".format(
2165                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2166                                item["baseCurrencyName"],
2167                                "+" if item["percentChanges"] > 0 else "",
2168                                float(item["percentChanges"]),
2169                            ),
2170                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2171                            item["action"],
2172                            item["type"],
2173                            item["date"],
2174                        ))
2175
2176                else:
2177                    info.append("\n## Total pending limit-orders: 0\n")
2178
2179                # --- Show stop orders section:
2180                if view["stat"]["stopOrders"]:
2181                    info.extend([
2182                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2183                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2184                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2185                    ])
2186
2187                    for item in view["stat"]["stopOrders"]:
2188                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2189                            "{} [{}]".format(item["ticker"], item["figi"]),
2190                            item["orderID"],
2191                            item["lotsRequested"],
2192                            "{} {} ({}{:.2f}%)".format(
2193                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2194                                item["baseCurrencyName"],
2195                                "+" if item["percentChanges"] > 0 else "",
2196                                float(item["percentChanges"]),
2197                            ),
2198                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2199                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2200                            item["action"],
2201                            item["type"],
2202                            item["expType"],
2203                            item["createDate"],
2204                            item["expDate"],
2205                        ))
2206
2207                else:
2208                    info.append("\n## Total stop-orders: 0\n")
2209
2210            if details in ["full", "analytics"]:
2211                # -- Show analytics section:
2212                if view["stat"]["portfolioCostRUB"] > 0:
2213                    info.extend([
2214                        "\n# Analytics\n"
2215                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2216                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2217                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2218                            view["stat"]["totalChangesRUB"],
2219                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2220                            view["stat"]["totalChangesPercentRUB"],
2221                        ),
2222                        "\n## Portfolio distribution by assets\n"
2223                        "\n| Type       | Uniques | Percent | Current cost       |\n",
2224                        "|------------|---------|---------|--------------------|\n",
2225                    ])
2226
2227                    for key in view["analytics"]["distrByAssets"].keys():
2228                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2229                            info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format(
2230                                key,
2231                                view["analytics"]["distrByAssets"][key]["uniques"],
2232                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2233                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2234                            ))
2235
2236                    maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()])
2237                    info.extend([
2238                        "\n## Portfolio distribution by companies\n"
2239                        "\n| Company{} | Percent | Current cost       |\n".format(" " * (maxLenNames - 7)),
2240                        "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)),
2241                    ])
2242
2243                    for company in view["analytics"]["distrByCompanies"].keys():
2244                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2245                            nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"])
2246                            info.append("| {} | {:<7} | {:<18} |\n".format(
2247                                "{}{}{}".format(
2248                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2249                                    company,
2250                                    "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)),
2251                                ),
2252                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2253                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2254                            ))
2255
2256                    maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()])
2257                    info.extend([
2258                        "\n## Portfolio distribution by sectors\n"
2259                        "\n| Sector{} | Percent | Current cost       |\n".format(" " * (maxLenSectors - 6)),
2260                        "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)),
2261                    ])
2262
2263                    for sector in view["analytics"]["distrBySectors"].keys():
2264                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2265                            info.append("| {}{} | {:<7} | {:<18} |\n".format(
2266                                sector,
2267                                "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)),
2268                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2269                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2270                            ))
2271
2272                    maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()])
2273                    info.extend([
2274                        "\n## Portfolio distribution by currencies\n"
2275                        "\n| Instruments currencies{} | Percent | Current cost       |\n".format(" " * (maxLenMoney - 22)),
2276                        "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)),
2277                    ])
2278
2279                    for curr in view["analytics"]["distrByCurrencies"].keys():
2280                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2281                            nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"])
2282                            info.append("| {} | {:<7} | {:<18} |\n".format(
2283                                "[{}] {}{}".format(
2284                                    curr,
2285                                    view["analytics"]["distrByCurrencies"][curr]["name"],
2286                                    "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen),
2287                                ),
2288                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2289                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2290                            ))
2291
2292                    maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()]))
2293                    info.extend([
2294                        "\n## Portfolio distribution by countries\n"
2295                        "\n| Assets by country{} | Percent | Current cost       |\n".format(" " * (maxLenCountry - 17)),
2296                        "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)),
2297                    ])
2298
2299                    for country in view["analytics"]["distrByCountries"].keys():
2300                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2301                            nameLen = len(country)
2302                            info.append("| {} | {:<7} | {:<18} |\n".format(
2303                                "{}{}".format(
2304                                    country,
2305                                    "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen),
2306                                ),
2307                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2308                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2309                            ))
2310
2311            infoText = "".join(info)
2312
2313            uLogger.info(infoText)
2314
2315            if details == "full" and self.overviewFile:
2316                filename = self.overviewFile
2317
2318            elif details == "digest" and self.overviewDigestFile:
2319                filename = self.overviewDigestFile
2320
2321            elif details == "positions" and self.overviewPositionsFile:
2322                filename = self.overviewPositionsFile
2323
2324            elif details == "orders" and self.overviewOrdersFile:
2325                filename = self.overviewOrdersFile
2326
2327            elif details == "analytics" and self.overviewAnalyticsFile:
2328                filename = self.overviewAnalyticsFile
2329
2330            else:
2331                filename = ""
2332
2333            if filename:
2334                with open(filename, "w", encoding="UTF-8") as fH:
2335                    fH.write(infoText)
2336
2337                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2338
2339        return view
2340
2341    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple:
2342        """
2343        Returns history operations between two given dates for current `accountId`.
2344        If `reportFile` string is not empty then also save human-readable report.
2345        Shows some statistical data of closed positions.
2346
2347        :param start: see docstring in `GetDatesAsString()` method
2348        :param end: see docstring in `GetDatesAsString()` method
2349        :param show: if `True` then also prints all records to the console.
2350        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2351        :return: original list of dictionaries with history of deals records from API ("operations" key):
2352                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2353                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2354        """
2355        if self.accountId is None or not self.accountId:
2356            uLogger.error("Variable `accountId` must be defined for using this method!")
2357            raise Exception("Account ID required")
2358
2359        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2360
2361        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2362
2363        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2364        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2365        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2366        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2367        customStat = {}  # custom statistics in additional to responseJSON
2368
2369        # --- output report in human-readable format:
2370        if show or self.reportFile:
2371            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2372            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2373            nextDay = ""
2374
2375            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2376
2377            if len(ops) > 0:
2378                customStat = {
2379                    "opsCount": 0,  # total operations count
2380                    "buyCount": 0,  # buy operations
2381                    "sellCount": 0,  # sell operations
2382                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2383                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2384                    "payIn": {"rub": 0.},  # Deposit brokerage account
2385                    "payOut": {"rub": 0.},  # Withdrawals
2386                    "divs": {"rub": 0.},  # Dividends income
2387                    "coupons": {"rub": 0.},  # Coupon's income
2388                    "brokerCom": {"rub": 0.},  # Service commissions
2389                    "serviceCom": {"rub": 0.},  # Service commissions
2390                    "marginCom": {"rub": 0.},  # Margin commissions
2391                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2392                }
2393
2394                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2395                for item in ops:
2396                    if item["state"] == "OPERATION_STATE_EXECUTED":
2397                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2398
2399                        # count buy operations:
2400                        if "_BUY" in item["operationType"]:
2401                            customStat["buyCount"] += 1
2402
2403                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2404                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2405
2406                            else:
2407                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2408
2409                        # count sell operations:
2410                        elif "_SELL" in item["operationType"]:
2411                            customStat["sellCount"] += 1
2412
2413                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2414                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2415
2416                            else:
2417                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2418
2419                        # count incoming operations:
2420                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2421                            if item["payment"]["currency"] in customStat["payIn"].keys():
2422                                customStat["payIn"][item["payment"]["currency"]] += payment
2423
2424                            else:
2425                                customStat["payIn"][item["payment"]["currency"]] = payment
2426
2427                        # count withdrawals operations:
2428                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2429                            if item["payment"]["currency"] in customStat["payOut"].keys():
2430                                customStat["payOut"][item["payment"]["currency"]] += payment
2431
2432                            else:
2433                                customStat["payOut"][item["payment"]["currency"]] = payment
2434
2435                        # count dividends income:
2436                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2437                            if item["payment"]["currency"] in customStat["divs"].keys():
2438                                customStat["divs"][item["payment"]["currency"]] += payment
2439
2440                            else:
2441                                customStat["divs"][item["payment"]["currency"]] = payment
2442
2443                        # count coupon's income:
2444                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2445                            if item["payment"]["currency"] in customStat["coupons"].keys():
2446                                customStat["coupons"][item["payment"]["currency"]] += payment
2447
2448                            else:
2449                                customStat["coupons"][item["payment"]["currency"]] = payment
2450
2451                        # count broker commissions:
2452                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2453                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2454                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2455
2456                            else:
2457                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2458
2459                        # count service commissions:
2460                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2461                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2462                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2463
2464                            else:
2465                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2466
2467                        # count margin commissions:
2468                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2469                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2470                                customStat["marginCom"][item["payment"]["currency"]] += payment
2471
2472                            else:
2473                                customStat["marginCom"][item["payment"]["currency"]] = payment
2474
2475                        # count withholding taxes:
2476                        elif "_TAX" in item["operationType"]:
2477                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2478                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2479
2480                            else:
2481                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2482
2483                        else:
2484                            continue
2485
2486                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2487
2488                # --- view "Actions" lines:
2489                info.extend([
2490                    "| 1                          | 2                             | 3                            | 4                    | 5                      |\n",
2491                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2492                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2493                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2494                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2495                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2496                    ),
2497                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2498                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2499                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2500                    ),
2501                ])
2502
2503                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2504                for key in opsKeys:
2505                    if key == "rub":
2506                        continue
2507
2508                    info.extend([
2509                        "|                            |                               | {:<28} |                      |                        |\n".format(
2510                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2511                        ),
2512                        "|                            |                               | {:<28} |                      |                        |\n".format(
2513                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2514                        ),
2515                    ])
2516
2517                info.append(splitLine1)
2518
2519                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2520                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2521                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2522                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2523                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2524                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2525                    )
2526
2527                # --- view "Payments" lines:
2528                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2529                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2530
2531                for key in paymentsKeys:
2532                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2533
2534                info.append(splitLine1)
2535
2536                # --- view "Commissions and taxes" lines:
2537                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2538                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2539
2540                for key in comKeys:
2541                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2542
2543                info.append(splitLine1)
2544
2545                info.extend([
2546                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2547                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2548                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2549                ])
2550
2551            else:
2552                info.append("Broker returned no operations during this period\n")
2553
2554            # --- view "Operations" section:
2555            for item in ops:
2556                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2557                    continue
2558
2559                else:
2560                    self.figi = item["figi"] if item["figi"] else ""
2561                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2562                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2563
2564                    # group of deals during one day:
2565                    if nextDay and item["date"].split("T")[0] != nextDay:
2566                        info.append(splitLine2)
2567                        nextDay = ""
2568
2569                    else:
2570                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2571
2572                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2573                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2574                        self.figi if self.figi else "—",
2575                        instrument["ticker"] if instrument else "—",
2576                        instrument["type"] if instrument else "—",
2577                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2578                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2579                        TKS_OPERATION_STATES[item["state"]],
2580                        TKS_OPERATION_TYPES[item["operationType"]],
2581                    ))
2582
2583            infoText = "".join(info)
2584
2585            if show:
2586                uLogger.info(infoText)
2587
2588            if self.reportFile:
2589                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2590                    fH.write(infoText)
2591
2592                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2593
2594        return ops, customStat
2595
2596    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2597        """
2598        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2599
2600        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2601        Warning! Broker server used ISO UTC time by default.
2602
2603        If `historyFile` is not `None` then method save history to file, otherwise return only pandas dataframe.
2604        Also, `historyFile` used to update history with `onlyMissing` parameter.
2605
2606        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2607
2608        :param start: see docstring in `GetDatesAsString()` method.
2609        :param end: see docstring in `GetDatesAsString()` method.
2610        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2611                         `"hour"`, `"day"`. Default: `"hour"`.
2612        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2613                            False by default. Warning! History appends only from last candle to current time
2614                            with always update last candle!
2615        :param csvSep: separator if csv-file is used, `,` by default.
2616        :param show: if `True` then also prints pandas dataframe to the console.
2617        :return: pandas dataframe with prices history. Headers of columns are defined by default:
2618                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2619        """
2620        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2621        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2622        history = None  # empty pandas object for history
2623
2624        if interval not in TKS_CANDLE_INTERVALS.keys():
2625            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2626            raise Exception("Incorrect value")
2627
2628        if not (self.ticker or self.figi):
2629            uLogger.error("Ticker or FIGI must be defined!")
2630            raise Exception("Ticker or FIGI required")
2631
2632        if self.ticker and not self.figi:
2633            instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False)
2634            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2635
2636        if self.figi and not self.ticker:
2637            instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False)
2638            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2639
2640        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2641        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2642        if interval.lower() != "day":
2643            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2644
2645        delta = dtEnd - dtStart  # current UTC time minus last time in file
2646        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2647
2648        # calculate history length in candles:
2649        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2650        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2651            length += 1  # to avoid fraction time
2652
2653        # calculate data blocks count:
2654        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2655
2656        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2657        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2658        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2659        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2660        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2661
2662        tempOld = None  # pandas object for old history, if --only-missing key present
2663        lastTime = None  # datetime object of last old candle in file
2664
2665        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2666            uLogger.debug("--only-missing key present, add only last missing candles...")
2667            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2668
2669            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2670
2671            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2672            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2673            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2674            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2675
2676            # get last datetime object from last string in file or minus 1 delta if file is empty:
2677            if len(tempOld) > 0:
2678                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2679
2680            else:
2681                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2682
2683            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2684
2685        responseJSONs = []  # raw history blocks of data
2686
2687        blockEnd = dtEnd
2688        for item in range(blocks):
2689            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2690            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2691
2692            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2693                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2694            ))
2695
2696            if blockStart == blockEnd:
2697                uLogger.debug("Skipped this zero-length block...")
2698
2699            else:
2700                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2701                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2702                self.body = str({
2703                    "figi": self.figi,
2704                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2705                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2706                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2707                })
2708                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False)
2709
2710                if "code" in responseJSON.keys():
2711                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2712
2713                else:
2714                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2715                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2716
2717                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2718
2719            blockEnd = blockStart
2720
2721        printCount = len(responseJSONs)  # candles to show in console
2722        if responseJSONs:
2723            tempHistory = pd.DataFrame(
2724                data={
2725                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2726                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2727                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2728                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2729                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2730                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2731                    "volume": [int(item["volume"]) for item in responseJSONs],
2732                },
2733                index=range(len(responseJSONs)),
2734                columns=["date", "time", "open", "high", "low", "close", "volume"],
2735            )
2736            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2737            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2738
2739            # append only newest candles to old history if --only-missing key present:
2740            if onlyMissing and tempOld is not None and lastTime is not None:
2741                index = 0  # find start index in tempHistory data:
2742
2743                for i, item in tempHistory.iterrows():
2744                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2745
2746                    if curTime == lastTime:
2747                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2748                        index = i
2749                        printCount = index + 1
2750                        break
2751
2752                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2753
2754            else:
2755                history = tempHistory  # if no `--only-missing` key then load full data from server
2756
2757            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2758
2759        if history is not None and not history.empty:
2760            if show:
2761                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2762                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2763                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2764                ))
2765
2766        else:
2767            uLogger.warning("Received an empty candles history!")
2768
2769        if self.historyFile is not None:
2770            if history is not None and not history.empty:
2771                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2772                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2773
2774            else:
2775                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2776
2777        else:
2778            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only pandas dataframe returns.")
2779
2780        return history
2781
2782    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2783        """
2784        Load candles history from csv-file and return pandas dataframe object.
2785
2786        See also: `History()` and `ShowHistoryChart()` methods.
2787
2788        :param filePath: path to csv-file to open.
2789        """
2790        loadedHistory = None  # init candles data object
2791
2792        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2793
2794        if os.path.exists(filePath):
2795            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as pandas dataframe
2796
2797            tfStr = self.priceModel.FormattedDelta(
2798                self.priceModel.timeframe,
2799                "{days} days {hours}h {minutes}m {seconds}s",
2800            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2801                self.priceModel.timeframe,
2802                "{hours}h {minutes}m {seconds}s",
2803            )
2804
2805            if loadedHistory is not None and not loadedHistory.empty:
2806                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2807                    len(loadedHistory),
2808                    tfStr,
2809                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2810                )
2811
2812            else:
2813                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2814
2815        else:
2816            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2817
2818        return loadedHistory
2819
2820    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2821        """
2822        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2823
2824        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2825        Default: `index.html` (both for interact and non-interact candlesticks chart).
2826
2827        See also: `History()` and `LoadHistory()` methods.
2828
2829        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2830        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2831                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2832                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2833                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2834        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2835                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2836        """
2837        if isinstance(candles, str):
2838            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2839            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2840
2841        elif isinstance(candles, pd.DataFrame):
2842            self.priceModel.prices = candles  # set candles chain from variable
2843            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2844
2845            if "datetime" not in candles.columns:
2846                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2847
2848        else:
2849            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2850            raise Exception("Incorrect value")
2851
2852        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2853
2854        if interact:
2855            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2856
2857            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2858
2859        else:
2860            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2861
2862            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2863
2864        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2865
2866    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2867        """
2868        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2869        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2870
2871        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2872
2873        :param operation: string "Buy" or "Sell".
2874        :param lots: volume, integer count of lots >= 1.
2875        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2876        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2877        :param expDate: string "Undefined" by default or local date in future,
2878                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2879        :return: JSON with response from broker server.
2880        """
2881        if self.accountId is None or not self.accountId:
2882            uLogger.error("Variable `accountId` must be defined for using this method!")
2883            raise Exception("Account ID required")
2884
2885        if operation is None or not operation or operation not in ("Buy", "Sell"):
2886            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2887            raise Exception("Incorrect value")
2888
2889        if lots is None or lots < 1:
2890            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2891            lots = 1
2892
2893        if tp is None or tp < 0:
2894            tp = 0
2895
2896        if sl is None or sl < 0:
2897            sl = 0
2898
2899        if expDate is None or not expDate:
2900            expDate = "Undefined"
2901
2902        if not (self.ticker or self.figi):
2903            uLogger.error("Ticker or FIGI must be defined!")
2904            raise Exception("Ticker or FIGI required")
2905
2906        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
2907        self.ticker = instrument["ticker"]
2908        self.figi = instrument["figi"]
2909
2910        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2911
2912        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2913        self.body = str({
2914            "figi": self.figi,
2915            "quantity": str(lots),
2916            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2917            "accountId": str(self.accountId),
2918            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2919        })
2920        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False)
2921
2922        if "orderId" in response.keys():
2923            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2924                operation, response["orderId"],
2925                self.ticker, self.figi, lots,
2926                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2927                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2928                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2929            ))
2930
2931        else:
2932            uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.")
2933
2934        if tp > 0:
2935            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2936
2937        if sl > 0:
2938            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2939
2940        return response
2941
2942    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2943        """
2944        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2945        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2946
2947        See also: `Order()` and `Trade()` docstrings.
2948
2949        :param lots: volume, integer count of lots >= 1.
2950        :param tp: float > 0, take profit price of stop-order.
2951        :param sl: float > 0, stop loss price of stop-order.
2952        :param expDate: it's a local date in future.
2953                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2954        :return: JSON with response from broker server.
2955        """
2956        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
2957
2958    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2959        """
2960        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2961        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2962
2963        See also: `Order()` and `Trade()` docstrings.
2964
2965        :param lots: volume, integer count of lots >= 1.
2966        :param tp: float > 0, take profit price of stop-order.
2967        :param sl: float > 0, stop loss price of stop-order.
2968        :param expDate: it's a local date in the future.
2969                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2970        :return: JSON with response from broker server.
2971        """
2972        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
2973
2974    def CloseTrades(self, tickers: list, portfolio: dict = None) -> None:
2975        """
2976        Close position of given instruments.
2977
2978        :param tickers: tickers list of instruments that must be closed.
2979        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
2980                         This avoids unnecessary downloading data from the server.
2981        """
2982        if not tickers:
2983            uLogger.info("Tickers list is empty, nothing to close.")
2984
2985        else:
2986            if portfolio is None or not portfolio:
2987                portfolio = self.Overview(show=False)
2988
2989            allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
2990            uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers))
2991
2992            for ticker in tickers:
2993                if ticker not in allOpenedTickers:
2994                    uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker))
2995                    continue
2996
2997                # search open trade info about instrument by ticker:
2998                instrument = {}
2999                for iType in TKS_INSTRUMENTS:
3000                    if instrument:
3001                        break
3002
3003                    for item in portfolio["stat"][iType]:
3004                        if item["ticker"] == ticker:
3005                            instrument = item
3006                            break
3007
3008                if instrument:
3009                    self.ticker = ticker
3010                    self.figi = instrument["figi"]
3011
3012                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3013                        self.ticker,
3014                        self.figi,
3015                        int(instrument["volume"]),
3016                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3017                    ))
3018
3019                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3020
3021                    if tradeLots > 0:
3022                        if instrument["blocked"] > 0:
3023                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3024                                instrument["blocked"],
3025                                self.ticker,
3026                                tradeLots,
3027                            ))
3028
3029                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3030                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3031
3032                    else:
3033                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
3034
3035    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3036        """
3037        Close all positions of given instruments with defined type.
3038
3039        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3040        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3041                         This avoids unnecessary downloading data from the server.
3042        """
3043        if iType not in TKS_INSTRUMENTS:
3044            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3045
3046        else:
3047            if portfolio is None or not portfolio:
3048                portfolio = self.Overview(show=False)
3049
3050            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3051            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3052
3053            if tickers and portfolio:
3054                self.CloseTrades(tickers, portfolio)
3055
3056            else:
3057                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3058
3059    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3060        """
3061        Universal method to create market or limit orders with all available parameters for current `accountId`.
3062        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3063
3064        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3065        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3066
3067        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3068        then broker immediately open market order as you can do simple --buy or --sell operations!
3069
3070        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3071        When current price will go up or down to target price value then broker opens a limit order.
3072        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3073
3074        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3075
3076        :param operation: string "Buy" or "Sell".
3077        :param orderType: string "Limit" or "Stop".
3078        :param lots: volume, integer count of lots >= 1.
3079        :param targetPrice: target price > 0. This is open trade price for limit order.
3080        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3081                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3082        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3083                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3084                         Stop loss order always executed by market price.
3085        :param expDate: string "Undefined" by default or local date in future.
3086                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3087                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3088                        A limit order has no expiration date, it lasts until the end of the trading day.
3089        :return: JSON with response from broker server.
3090        """
3091        if self.accountId is None or not self.accountId:
3092            uLogger.error("Variable `accountId` must be defined for using this method!")
3093            raise Exception("Account ID required")
3094
3095        if operation is None or not operation or operation not in ("Buy", "Sell"):
3096            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3097            raise Exception("Incorrect value")
3098
3099        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3100            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3101            raise Exception("Incorrect value")
3102
3103        if lots is None or lots < 1:
3104            uLogger.error("You must define trade volume > 0: integer count of lots!")
3105            raise Exception("Incorrect value")
3106
3107        if targetPrice is None or targetPrice <= 0:
3108            uLogger.error("Target price for limit-order must be greater than 0!")
3109            raise Exception("Incorrect value")
3110
3111        if limitPrice is None or limitPrice <= 0:
3112            limitPrice = targetPrice
3113
3114        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3115            stopType = "Limit"
3116
3117        if expDate is None or not expDate:
3118            expDate = "Undefined"
3119
3120        if not (self.ticker or self.figi):
3121            uLogger.error("Tocker or FIGI must be defined!")
3122            raise Exception("Ticker or FIGI required")
3123
3124        response = {}
3125        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
3126        self.ticker = instrument["ticker"]
3127        self.figi = instrument["figi"]
3128
3129        if orderType == "Limit":
3130            uLogger.debug(
3131                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3132                    self.ticker, self.figi,
3133                    operation, lots, targetPrice, instrument["currency"],
3134                ))
3135
3136            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3137            self.body = str({
3138                "figi": self.figi,
3139                "quantity": str(lots),
3140                "price": FloatToNano(targetPrice),
3141                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3142                "accountId": str(self.accountId),
3143                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3144            })
3145            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3146
3147            if "orderId" in response.keys():
3148                uLogger.info(
3149                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3150                        response["orderId"],
3151                        self.ticker, self.figi,
3152                        operation, lots, targetPrice, instrument["currency"],
3153                    ))
3154
3155                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3156                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3157                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3158                            targetPrice, instrument["currency"],
3159                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3160                        ))
3161
3162                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3163                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3164                            targetPrice, instrument["currency"],
3165                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3166                        ))
3167
3168            else:
3169                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3170
3171        if orderType == "Stop":
3172            uLogger.debug(
3173                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3174                    self.ticker, self.figi,
3175                    operation, lots,
3176                    targetPrice, instrument["currency"],
3177                    limitPrice, instrument["currency"],
3178                    stopType, expDate,
3179                ))
3180
3181            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3182            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3183            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3184
3185            body = {
3186                "figi": self.figi,
3187                "quantity": str(lots),
3188                "price": FloatToNano(limitPrice),
3189                "stopPrice": FloatToNano(targetPrice),
3190                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3191                "accountId": str(self.accountId),
3192                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3193                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3194            }
3195
3196            if expDateUTC:
3197                body["expireDate"] = expDateUTC
3198
3199            self.body = str(body)
3200            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3201
3202            if "stopOrderId" in response.keys():
3203                uLogger.info(
3204                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3205                        response["stopOrderId"],
3206                        self.ticker, self.figi,
3207                        operation, lots,
3208                        targetPrice, instrument["currency"],
3209                        limitPrice, instrument["currency"],
3210                        TKS_STOP_ORDER_TYPES[stopOrderType],
3211                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3212                    ))
3213
3214                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3215                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3216                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3217                            targetPrice, instrument["currency"],
3218                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3219                        ))
3220
3221                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3222                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3223                            targetPrice, instrument["currency"],
3224                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3225                        ))
3226
3227            else:
3228                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3229
3230        return response
3231
3232    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3233        """
3234        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3235        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3236        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3237        See also: `Order()` docstring.
3238
3239        :param lots: volume, integer count of lots >= 1.
3240        :param targetPrice: target price > 0. This is open trade price for limit order.
3241        :return: JSON with response from broker server.
3242        """
3243        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3244
3245    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3246        """
3247        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3248        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3249        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3250        target price value then broker opens a limit order. See also: `Order()` docstring.
3251
3252        :param lots: volume, integer count of lots >= 1.
3253        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3254        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3255                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3256        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3257                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3258        :param expDate: string "Undefined" by default or local date in future.
3259                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3260                        This date is converting to UTC format for server.
3261        :return: JSON with response from broker server.
3262        """
3263        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3264
3265    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3266        """
3267        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3268        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3269        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3270        See also: `Order()` docstring.
3271
3272        :param lots: volume, integer count of lots >= 1.
3273        :param targetPrice: target price > 0. This is open trade price for limit order.
3274        :return: JSON with response from broker server.
3275        """
3276        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3277
3278    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3279        """
3280        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3281        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3282        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3283        target price value then broker opens a limit order. See also: `Order()` docstring.
3284
3285        :param lots: volume, integer count of lots >= 1.
3286        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3287        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3288                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3289        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3290                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3291        :param expDate: string "Undefined" by default or local date in future.
3292                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3293                        This date is converting to UTC format for server.
3294        :return: JSON with response from broker server.
3295        """
3296        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3297
3298    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3299        """
3300        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3301
3302        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3303        :param allOrdersIDs: pre-received lists of all active pending orders.
3304                             This avoids unnecessary downloading data from the server.
3305        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3306        """
3307        if self.accountId is None or not self.accountId:
3308            uLogger.error("Variable `accountId` must be defined for using this method!")
3309            raise Exception("Account ID required")
3310
3311        if orderIDs:
3312            if allOrdersIDs is None or not allOrdersIDs:
3313                rawOrders = self.RequestPendingOrders()
3314                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3315
3316            if allStopOrdersIDs is None or not allStopOrdersIDs:
3317                rawStopOrders = self.RequestStopOrders()
3318                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3319
3320            for orderID in orderIDs:
3321                idInPendingOrders = orderID in allOrdersIDs
3322                idInStopOrders = orderID in allStopOrdersIDs
3323
3324                if not (idInPendingOrders or idInStopOrders):
3325                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3326                    continue
3327
3328                else:
3329                    if idInPendingOrders:
3330                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3331
3332                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3333                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3334                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3335                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3336
3337                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3338                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3339                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3340
3341                        else:
3342                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3343
3344                    elif idInStopOrders:
3345                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3346
3347                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3348                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3349                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3350                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3351
3352                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3353                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3354                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3355
3356                        else:
3357                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3358
3359                    else:
3360                        continue
3361
3362    def CloseAllOrders(self) -> None:
3363        """
3364        Gets a list of open pending and stop orders and cancel it all.
3365        """
3366        rawOrders = self.RequestPendingOrders()
3367        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3368        lenOrders = len(allOrdersIDs)
3369
3370        rawStopOrders = self.RequestStopOrders()
3371        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3372        lenSOrders = len(allStopOrdersIDs)
3373
3374        if lenOrders > 0 or lenSOrders > 0:
3375            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3376
3377            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3378
3379        else:
3380            uLogger.info("Orders not found, nothing to cancel.")
3381
3382    def CloseAll(self, *args) -> None:
3383        """
3384        Close all available (not blocked) opened trades and orders.
3385
3386        Also, you can select one or more keywords case-insensitive:
3387        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3388
3389        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3390        """
3391        overview = self.Overview(show=False)  # get all open trades info
3392
3393        if len(args) == 0:
3394            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3395            self.CloseAllOrders()  # close all pending and stop orders
3396
3397            for iType in TKS_INSTRUMENTS:
3398                if iType != "Currencies":
3399                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3400
3401        else:
3402            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3403            lowerArgs = [x.lower() for x in args]
3404
3405            if "orders" in lowerArgs:
3406                self.CloseAllOrders()  # close all pending and stop orders
3407
3408            for iType in TKS_INSTRUMENTS:
3409                if iType.lower() in lowerArgs and iType != "Currencies":
3410                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3411
3412    @staticmethod
3413    def ParseOrderParameters(operation, **inputParameters):
3414        """
3415        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3416
3417        :param operation: string "Buy" or "Sell".
3418        :param inputParameters: this is dict of strings that looks like this
3419               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3420               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3421               "prices" key: one or more prices to open limit-orders
3422               Counts of values in lots and prices lists must be equals!
3423        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3424        """
3425        # TODO: update order grid work with api v2
3426        pass
3427        # uLogger.debug("Input parameters: {}".format(inputParameters))
3428        #
3429        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3430        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3431        #     raise Exception("Incorrect value")
3432        #
3433        # if "l" in inputParameters.keys():
3434        #     inputParameters["lots"] = inputParameters.pop("l")
3435        #
3436        # if "p" in inputParameters.keys():
3437        #     inputParameters["prices"] = inputParameters.pop("p")
3438        #
3439        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3440        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3441        #     raise Exception("Incorrect value")
3442        #
3443        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3444        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3445        #
3446        # if len(lots) != len(prices):
3447        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3448        #     raise Exception("Incorrect value")
3449        #
3450        # uLogger.debug("Extracted parameters for orders:")
3451        # uLogger.debug("lots = {}".format(lots))
3452        # uLogger.debug("prices = {}".format(prices))
3453        #
3454        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3455        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3456        # uLogger.debug("Order parameters: {}".format(result))
3457        #
3458        # return result
3459
3460    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3461        """
3462        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3463
3464        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3465        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3466        """
3467        result = False
3468        msg = "Instrument not defined!"
3469
3470        if portfolio is None or not portfolio:
3471            portfolio = self.Overview(show=False)
3472
3473        if self.ticker:
3474            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3475            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3476
3477            for iType in TKS_INSTRUMENTS:
3478                for instrument in portfolio["stat"][iType]:
3479                    if instrument["ticker"] == self.ticker:
3480                        result = True
3481                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3482                        break
3483
3484        elif self.figi:
3485            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3486            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3487
3488            for iType in TKS_INSTRUMENTS:
3489                for instrument in portfolio["stat"][iType]:
3490                    if instrument["figi"] == self.figi:
3491                        result = True
3492                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3493                        break
3494
3495        else:
3496            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3497
3498        uLogger.debug(msg)
3499
3500        return result
3501
3502    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3503        """
3504        Returns instrument is in the user's portfolio if it presents there.
3505        Instrument must be defined by `ticker` (highly priority) or `figi`.
3506
3507        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3508        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3509        """
3510        result = None
3511        msg = "Instrument not defined!"
3512
3513        if portfolio is None or not portfolio:
3514            portfolio = self.Overview(show=False)
3515
3516        if self.ticker:
3517            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3518            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3519
3520            for iType in TKS_INSTRUMENTS:
3521                for instrument in portfolio["stat"][iType]:
3522                    if instrument["ticker"] == self.ticker:
3523                        result = instrument
3524                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3525                        break
3526
3527        elif self.figi:
3528            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3529            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3530
3531            for iType in TKS_INSTRUMENTS:
3532                for instrument in portfolio["stat"][iType]:
3533                    if instrument["figi"] == self.figi:
3534                        result = instrument
3535                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3536                        break
3537
3538        else:
3539            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3540
3541        uLogger.debug(msg)
3542
3543        return result
3544
3545    def RequestLimits(self) -> dict:
3546        """
3547        Method for obtaining the available funds for withdrawal for current `accountId`.
3548
3549        See also:
3550        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3551        - `OverviewLimits()` method
3552
3553        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3554                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3555                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3556                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3557        """
3558        if self.accountId is None or not self.accountId:
3559            uLogger.error("Variable `accountId` must be defined for using this method!")
3560            raise Exception("Account ID required")
3561
3562        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3563
3564        self.body = str({"accountId": self.accountId})
3565        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3566        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3567
3568        uLogger.debug("Records about available funds for withdrawal successfully received")
3569
3570        return rawLimits
3571
3572    def OverviewLimits(self, show: bool = False) -> dict:
3573        """
3574        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3575
3576        See also: `RequestLimits()`.
3577
3578        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3579        :return: dict with raw parsed data from server and some calculated statistics about it.
3580        """
3581        if self.accountId is None or not self.accountId:
3582            uLogger.error("Variable `accountId` must be defined for using this method!")
3583            raise Exception("Account ID required")
3584
3585        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3586
3587        view = {
3588            "rawLimits": rawLimits,
3589            "limits": {  # parsed data for every currency:
3590                "money": {  # this is an array of portfolio currency positions
3591                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3592                },
3593                "blocked": {  # this is an array of blocked currency
3594                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3595                },
3596                "blockedGuarantee": {  # this is locked money under collateral for futures
3597                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3598                },
3599            },
3600        }
3601
3602        # --- Prepare text table with limits in human-readable format:
3603        if show:
3604            info = [
3605                "# Withdrawal limits\n\n",
3606                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3607                "* **Account ID:** [{}]\n".format(self.accountId),
3608                "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3609                "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3610            ]
3611
3612            for curr in view["limits"]["money"].keys():
3613                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3614                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3615                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3616
3617                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3618                    "[{}]".format(curr),
3619                    "{:.2f}".format(view["limits"]["money"][curr]),
3620                    "{:.2f}".format(availableMoney),
3621                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3622                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3623                )
3624
3625                if curr == "rub":
3626                    info.insert(5, infoStr)  # insert at first position in table and after headers
3627
3628                else:
3629                    info.append(infoStr)
3630
3631            infoText = "".join(info)
3632
3633            uLogger.info(infoText)
3634
3635            if self.withdrawalLimitsFile:
3636                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3637                    fH.write(infoText)
3638
3639                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3640
3641        return view
3642
3643    def RequestAccounts(self) -> dict:
3644        """
3645        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3646
3647        See also:
3648        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3649        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3650        - `OverviewUserInfo()` method
3651
3652        :return: dict with raw data from server that contains accounts info. Example of dict:
3653                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3654                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3655                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3656                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3657        """
3658        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3659
3660        self.body = str({})
3661        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3662        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3663
3664        uLogger.debug("Records about available accounts successfully received")
3665
3666        return rawAccounts
3667
3668    def RequestUserInfo(self) -> dict:
3669        """
3670        Method for requesting common user's information.
3671
3672        See also:
3673        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3674        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3675        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3676        - `OverviewUserInfo()` method
3677
3678        :return: dict with raw data from server that contains user's information. Example of dict:
3679                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3680                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3681        """
3682        uLogger.debug("Requesting common user's information. Wait, please...")
3683
3684        self.body = str({})
3685        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3686        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3687
3688        uLogger.debug("Records about current user successfully received")
3689
3690        return rawUserInfo
3691
3692    def RequestMarginStatus(self, accountId: str = None) -> dict:
3693        """
3694        Method for requesting margin calculation for defined account ID.
3695
3696        See also:
3697        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3698        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3699        - `OverviewUserInfo()` method
3700
3701        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3702        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3703                 Example of responses:
3704                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3705                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3706                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3707                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3708                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3709                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3710        """
3711        if accountId is None or not accountId:
3712            if self.accountId is None or not self.accountId:
3713                uLogger.error("Variable `accountId` must be defined for using this method!")
3714                raise Exception("Account ID required")
3715
3716            else:
3717                accountId = self.accountId  # use `self.accountId` (main ID) by default
3718
3719        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3720
3721        self.body = str({"accountId": accountId})
3722        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3723        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3724
3725        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3726            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3727            rawMargin = {}
3728
3729        else:
3730            uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3731
3732        return rawMargin
3733
3734    def RequestTariffLimits(self) -> dict:
3735        """
3736        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3737
3738        See also:
3739        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3740        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3741        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3742        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3743        - `OverviewUserInfo()` method
3744
3745        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3746                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3747                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3748        """
3749        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3750
3751        self.body = str({})
3752        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3753        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3754
3755        uLogger.debug("Records with limits of current tariff successfully received")
3756
3757        return rawTariffLimits
3758
3759    def RequestBondCoupons(self, iJSON: dict) -> dict:
3760        """
3761        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3762        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3763        All dates are in UTC timezone.
3764
3765        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3766        Documentation:
3767        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3768        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3769
3770        See also: `ExtendBondsData()`.
3771
3772        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3773                      If raw iJSON is not data of bond then server returns an error [400] with message:
3774                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3775        :return: dictionary with bond payment calendar. Response example
3776                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3777                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3778                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3779                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3780        """
3781        if iJSON["figi"] is None or not iJSON["figi"]:
3782            uLogger.error("FIGI must be defined for using this method!")
3783            raise Exception("FIGI required")
3784
3785        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3786        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3787
3788        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3789            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3790            self.figi,
3791            startDate,
3792            endDate,
3793        ))
3794
3795        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3796        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3797        calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False)
3798
3799        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3800            uLogger.warning("Instrument type is not bond!")
3801
3802        else:
3803            uLogger.debug("Records about bond payment calendar successfully received")
3804
3805        return calendar
3806
3807    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3808        """
3809        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3810        pandas dataframe with more information about bonds: main info, current prices, bond payment calendar,
3811        coupon yields, current yields and some statistics etc.
3812
3813        WARNING! This is too long operation if a lot of bonds requested from broker server.
3814
3815        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3816
3817        :param instruments: list of strings with tickers or FIGIs.
3818        :param xlsx: if True then also exports pandas dataframe to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3819                     for further used by data scientists or stock analytics.
3820        :return: wider pandas dataframe with more full and calculated data about bonds, than raw response from broker.
3821                 In XLSX-file and pandas dataframe fields mean:
3822                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3823                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3824        """
3825        if instruments is None or not instruments:
3826            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3827            raise Exception("Ticker or FIGI required")
3828
3829        if isinstance(instruments, str):
3830            instruments = [instruments]
3831
3832        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3833
3834        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3835
3836        iCount = len(uniqueInstruments)
3837        tooLong = iCount >= 20
3838        if tooLong:
3839            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3840
3841        bonds = None
3842        for i, self.figi in enumerate(uniqueInstruments):
3843            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3844
3845            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3846                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3847                rawBond = self.SearchByFIGI(requestPrice=True)
3848
3849                # Widen raw data with UTC current time (iData["actualDateTime"]):
3850                actualDate = datetime.now(tzutc())
3851                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3852
3853                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3854                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3855
3856                # Replace some values with human-readable:
3857                iData["nominalCurrency"] = iData["nominal"]["currency"]
3858                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3859                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3860                iData["aciCurrency"] = iData["aciValue"]["currency"]
3861                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3862                iData["issueSize"] = int(iData["issueSize"])
3863                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3864                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3865                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3866                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3867                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3868                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3869                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3870                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3871                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3872                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3873
3874                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3875                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3876                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3877                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3878                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3879                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3880                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3881                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3882                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3883                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3884                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3885
3886                # Widen raw data with calendar data from `rawCalendar` values:
3887                calendarData = []
3888                for item in iData["rawCalendar"]["events"]:
3889                    calendarData.append({
3890                        "couponDate": item["couponDate"],
3891                        "couponNumber": int(item["couponNumber"]),
3892                        "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3893                        "payCurrency": item["payOneBond"]["currency"],
3894                        "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3895                        "couponType": TKS_COUPON_TYPES[item["couponType"]],
3896                        "couponStartDate": item["couponStartDate"],
3897                        "couponEndDate": item["couponEndDate"],
3898                        "couponPeriod": item["couponPeriod"],
3899                    })
3900
3901                # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3902                if "maturityDate" not in iData.keys():
3903                    iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3904
3905                # Widen raw data with Coupon Rate.
3906                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3907                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3908                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3909                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3910
3911                # Widen raw data with Yield to Maturity (YTM) on current date.
3912                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3913                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3914                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3915                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3916                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3917                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3918
3919                iData["calendar"] = calendarData  # adds calendar at the end
3920
3921                # Remove not used data:
3922                iData.pop("uid")
3923                iData.pop("positionUid")
3924                iData.pop("currentPrice")
3925                iData.pop("rawCalendar")
3926
3927                colNames = list(iData.keys())
3928                if bonds is None:
3929                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3930
3931                else:
3932                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
3933
3934            else:
3935                uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"]))
3936
3937            processed = round(100 * (i + 1) / iCount, 1)
3938            if tooLong and processed % 5 == 0:
3939                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
3940
3941            else:
3942                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
3943
3944        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
3945
3946        # Saving bonds from pandas dataframe to XLSX sheet:
3947        if xlsx and self.bondsXLSXFile:
3948            with pd.ExcelWriter(
3949                    path=self.bondsXLSXFile,
3950                    date_format=TKS_DATE_FORMAT,
3951                    datetime_format=TKS_DATE_TIME_FORMAT,
3952                    mode="w",
3953            ) as writer:
3954                bonds.to_excel(
3955                    writer,
3956                    sheet_name="Extended bonds data",
3957                    index=True,
3958                    encoding="UTF-8",
3959                    freeze_panes=(1, 1),
3960                )  # saving as XLSX-file with freeze first row and column as headers
3961
3962            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
3963
3964        return bonds
3965
3966    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
3967        """
3968        Creates bond payments calendar as pandas dataframe, and also save it to the XLSX-file, `calendar.xlsx` by default.
3969
3970        WARNING! This is too long operation if a lot of bonds requested from broker server.
3971
3972        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
3973
3974        :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains
3975                        extended information about bonds: main info, current prices, bond payment calendar,
3976                        coupon yields, current yields and some statistics etc.
3977                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
3978        :param xlsx: if True then also exports pandas dataframe to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
3979                     for further used by data scientists or stock analytics.
3980        :return: pandas dataframe with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
3981        """
3982        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
3983            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
3984
3985        uLogger.debug("Generating bond payments calendar data. Wait, please...")
3986
3987        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
3988        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
3989        calendar = None
3990        for bond in extBonds.iterrows():
3991            for item in bond[1]["calendar"]:
3992                cData = {
3993                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
3994                    "couponDate": item["couponDate"],
3995                    "figi": bond[1]["figi"],
3996                    "ticker": bond[1]["ticker"],
3997                    "name": bond[1]["name"],
3998                    "couponNumber": item["couponNumber"],
3999                    "payOneBond": item["payOneBond"],
4000                    "payCurrency": item["payCurrency"],
4001                    "couponType": item["couponType"],
4002                    "couponPeriod": item["couponPeriod"],
4003                    "fixDate": item["fixDate"],
4004                    "couponStartDate": item["couponStartDate"],
4005                    "couponEndDate": item["couponEndDate"],
4006                }
4007
4008                if calendar is None:
4009                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4010
4011                else:
4012                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4013
4014        calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4015
4016        # Saving calendar from pandas dataframe to XLSX sheet:
4017        if xlsx:
4018            xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4019
4020            with pd.ExcelWriter(
4021                    path=xlsxCalendarFile,
4022                    date_format=TKS_DATE_FORMAT,
4023                    datetime_format=TKS_DATE_TIME_FORMAT,
4024                    mode="w",
4025            ) as writer:
4026                humanReadable = calendar.copy(deep=True)
4027                humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4028                humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4029                humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4030                humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4031                humanReadable.columns = colNames  # human-readable column names
4032
4033                humanReadable.to_excel(
4034                    writer,
4035                    sheet_name="Bond payments calendar",
4036                    index=False,
4037                    encoding="UTF-8",
4038                    freeze_panes=(1, 2),
4039                )  # saving as XLSX-file with freeze first row and column as headers
4040
4041                del humanReadable  # release df in memory
4042
4043            uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4044
4045        return calendar
4046
4047    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4048        """
4049        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4050        Also, creates Markdown file with calendar data, `calendar.md` by default.
4051
4052        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4053
4054        :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains
4055                        extended information about bonds: main info, current prices, bond payment calendar,
4056                        coupon yields, current yields and some statistics etc.
4057                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4058        :param show: if `True` then also printing bonds payment calendar to the console,
4059                     otherwise save to file `calendarFile` only. `False` by default.
4060        :return: multilines text in Markdown format with bonds payment calendar as a table.
4061        """
4062        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4063            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4064
4065        infoText = "# Bond payments calendar\n\n"
4066
4067        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate pandas dataframe with full calendar data
4068
4069        if not calendar.empty:
4070            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4071
4072            info = [
4073                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4074                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4075            ]
4076
4077            newMonth = False
4078            notOneBond = calendar["figi"].nunique() > 1
4079            for i, bond in enumerate(calendar.iterrows()):
4080                if newMonth and notOneBond:
4081                    info.append(splitLine)
4082
4083                info.append(
4084                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4085                        "  +" if bond[1]["paid"] else "  —",
4086                        bond[1]["couponDate"].split("T")[0],
4087                        bond[1]["figi"],
4088                        bond[1]["ticker"],
4089                        bond[1]["couponNumber"],
4090                        "{} {}".format(
4091                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4092                            bond[1]["payCurrency"],
4093                        ),
4094                        bond[1]["couponType"],
4095                        bond[1]["couponPeriod"],
4096                        bond[1]["fixDate"].split("T")[0],
4097                    )
4098                )
4099
4100                if i < len(calendar.values) - 1:
4101                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4102                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4103                    newMonth = False if curDate.month == nextDate.month else True
4104
4105                else:
4106                    newMonth = False
4107
4108            infoText += "".join(info)
4109
4110            if show:
4111                uLogger.info("{}".format(infoText))
4112
4113            if self.calendarFile is not None:
4114                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4115                    fH.write(infoText)
4116
4117                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4118
4119        else:
4120            infoText += "No data\n"
4121
4122        return infoText
4123
4124    def OverviewAccounts(self, show: bool = False) -> dict:
4125        """
4126        Method for parsing and show simple table with all available user accounts.
4127
4128        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4129
4130        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4131        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4132                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4133                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4134                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4135                                                        "closed": "—", "access": "Full access" }, ...}}`
4136        """
4137        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4138
4139        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4140        accounts = {
4141            item["id"]: {
4142                "type": TKS_ACCOUNT_TYPES[item["type"]],
4143                "name": item["name"],
4144                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4145                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4146                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4147                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4148            } for item in rawAccounts["accounts"]
4149        }
4150
4151        # Raw and parsed data with some fields replaced in "stat" section:
4152        view = {
4153            "rawAccounts": rawAccounts,
4154            "stat": accounts,
4155        }
4156
4157        # --- Prepare simple text table with only accounts data in human-readable format:
4158        if show:
4159            info = [
4160                "# User accounts\n\n",
4161                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4162                "| Account ID   | Type                      | Status                    | Name                           |\n",
4163                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4164            ]
4165
4166            for account in view["stat"].keys():
4167                info.extend([
4168                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4169                        account,
4170                        view["stat"][account]["type"],
4171                        view["stat"][account]["status"],
4172                        view["stat"][account]["name"],
4173                    )
4174                ])
4175
4176            infoText = "".join(info)
4177
4178            uLogger.info(infoText)
4179
4180            if self.userAccountsFile:
4181                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4182                    fH.write(infoText)
4183
4184                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4185
4186        return view
4187
4188    def OverviewUserInfo(self, show: bool = False) -> dict:
4189        """
4190        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4191
4192        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4193
4194        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4195        :return: dict with raw parsed data from server and some calculated statistics about it.
4196        """
4197        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4198        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4199        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4200        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4201        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4202        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4203
4204        # This is dict with parsed common user data:
4205        userInfo = {
4206            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4207            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4208            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4209            "tariff": rawUserInfo["tariff"],
4210        }
4211
4212        # This is an array of dict with parsed margin statuses for every account IDs:
4213        margins = {}
4214        for accountId in accounts.keys():
4215            if rawMargins[accountId]:
4216                margins[accountId] = {
4217                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4218                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4219                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4220                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4221                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4222                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4223                }
4224
4225            else:
4226                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4227
4228        unary = {}  # unary-connection limits
4229        for item in rawTariffLimits["unaryLimits"]:
4230            if item["limitPerMinute"] in unary.keys():
4231                unary[item["limitPerMinute"]].extend(item["methods"])
4232
4233            else:
4234                unary[item["limitPerMinute"]] = item["methods"]
4235
4236        stream = {}  # stream-connection limits
4237        for item in rawTariffLimits["streamLimits"]:
4238            if item["limit"] in stream.keys():
4239                stream[item["limit"]].extend(item["streams"])
4240
4241            else:
4242                stream[item["limit"]] = item["streams"]
4243
4244        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4245        limits = {
4246            "unary": unary,
4247            "stream": stream,
4248        }
4249
4250        # Raw and parsed data as an output result:
4251        view = {
4252            "rawUserInfo": rawUserInfo,
4253            "rawAccounts": rawAccounts,
4254            "rawMargins": rawMargins,
4255            "rawTariffLimits": rawTariffLimits,
4256            "stat": {
4257                "userInfo": userInfo,
4258                "accounts": accounts,
4259                "margins": margins,
4260                "limits": limits,
4261            },
4262        }
4263
4264        # --- Prepare text table with user information in human-readable format:
4265        if show:
4266            info = [
4267                "# Full user information\n\n",
4268                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4269                "## Common information\n\n",
4270                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4271                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4272                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4273                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4274                "\n## User accounts\n\n",
4275            ]
4276
4277            for account in view["stat"]["accounts"].keys():
4278                info.extend([
4279                    "### ID: [{}]\n\n".format(account),
4280                    "| Parameters           | Values                                                       |\n",
4281                    "|----------------------|--------------------------------------------------------------|\n",
4282                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4283                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4284                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4285                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4286                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4287                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4288                ])
4289
4290                if margins[account]:
4291                    info.extend([
4292                        "| Margin status:       | Enabled                                                      |\n",
4293                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4294                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4295                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4296                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4297                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4298                    ])
4299
4300                else:
4301                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4302
4303            info.extend([
4304                "\n## Current user tariff limits\n",
4305                "\nSee also:\n",
4306                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4307                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4308                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4309                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4310                "\n### Unary limits\n",
4311            ])
4312
4313            if unary:
4314                for key, values in sorted(unary.items()):
4315                    info.append("\n* Max requests per minute: {}\n".format(key))
4316
4317                    for value in values:
4318                        info.append("  - {}\n".format(value))
4319
4320            else:
4321                info.append("\nNot available\n")
4322
4323            info.append("\n### Stream limits\n")
4324
4325            if stream:
4326                for key, values in sorted(stream.items()):
4327                    info.append("\n* Max stream connections: {}\n".format(key))
4328
4329                    for value in values:
4330                        info.append("  - {}\n".format(value))
4331
4332            else:
4333                info.append("\nNot available\n")
4334
4335            infoText = "".join(info)
4336
4337            uLogger.info(infoText)
4338
4339            if self.userInfoFile:
4340                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4341                    fH.write(infoText)
4342
4343                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4344
4345        return view

This class implements methods to work with Tinkoff broker server.

Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/

About token: https://tinkoff.github.io/investAPI/token/

TinkoffBrokerServer( token: str, accountId: str = None, useCache: bool = True, defaultCache: str = 'dump.json')
196    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
197        """
198        Main class init.
199
200        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
201        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
202                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
203        :param useCache: use default cache file with raw data to use instead of `iList`.
204                         True by default. Cache is auto-update if new day has come.
205                         If you don't want to use cache and always updates raw data then set `useCache=False`.
206        :param defaultCache: path to default cache file. `dump.json` by default.
207        """
208        if token is None or not token:
209            try:
210                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
211                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
212
213            except KeyError:
214                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
215                raise Exception("Token required")
216
217        else:
218            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
219            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
220
221        if accountId is None or not accountId:
222            try:
223                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
224                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
225
226            except KeyError:
227                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
228
229        else:
230            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
231            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
232
233        self.version = __version__  # duplicate here used TKSBrokerAPI main version
234        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
235
236        Latest version: https://pypi.org/project/tksbrokerapi/
237        """
238
239        self.aliases = TKS_TICKER_ALIASES
240        """Some aliases instead official tickers.
241
242        See also: `TKSEnums.TKS_TICKER_ALIASES`
243        """
244
245        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
246
247        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
248
249        self.ticker = ""
250        """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
251
252        See also: `SearchByTicker()`, `SearchInstruments()`.
253        """
254
255        self.figi = ""
256        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`.
257
258        See also: `SearchByFIGI()`, `SearchInstruments()`.
259        """
260
261        self.depth = 1
262        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
263
264        See also: `GetCurrentPrices()`.
265        """
266
267        self.server = r"https://invest-public-api.tinkoff.ru/rest"
268        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
269
270        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
271        """
272
273        uLogger.debug("Broker API server: {}".format(self.server))
274
275        self.timeout = 15
276        """Server operations timeout in seconds. Default: `15`.
277
278        See also: `SendAPIRequest()`.
279        """
280
281        self.headers = {
282            "Content-Type": "application/json",
283            "accept": "application/json",
284            "Authorization": "Bearer {}".format(self.token),
285            "x-app-name": "Tim55667757.TKSBrokerAPI",
286        }
287        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
288
289        See also: `SendAPIRequest()`.
290        """
291
292        self.body = None
293        """Request body which send to broker server. Default: `None`.
294
295        See also: `SendAPIRequest()`.
296        """
297
298        self.historyFile = None
299        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only pandas dataframe.
300
301        See also: `History()`.
302        """
303
304        self.htmlHistoryFile = "index.html"
305        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
306
307        See also: `ShowHistoryChart()`.
308        """
309
310        self.instrumentsFile = "instruments.md"
311        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
312
313        See also: `ShowInstrumentsInfo()`.
314        """
315
316        self.searchResultsFile = "search-results.md"
317        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
318
319        See also: `SearchInstruments()`.
320        """
321
322        self.pricesFile = "prices.md"
323        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
324
325        See also: `GetListOfPrices()`.
326        """
327
328        self.infoFile = "info.md"
329        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
330
331        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
332        """
333
334        self.bondsXLSXFile = "ext-bonds.xlsx"
335        """Filename where wider pandas dataframe with more information about bonds: main info, current prices, 
336        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
337
338        See also: `ExtendBondsData()`.
339        """
340
341        self.calendarFile = "calendar.md"
342        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
343        
344        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
345
346        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
347        """
348
349        self.overviewFile = "overview.md"
350        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
351
352        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
353        """
354
355        self.overviewDigestFile = "overview-digest.md"
356        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
357
358        See also: `Overview()` with parameter `details="digest"`.
359        """
360
361        self.overviewPositionsFile = "overview-positions.md"
362        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
363
364        See also: `Overview()` with parameter `details="positions"`.
365        """
366
367        self.overviewOrdersFile = "overview-orders.md"
368        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
369
370        See also: `Overview()` with parameter `details="orders"`.
371        """
372
373        self.overviewAnalyticsFile = "overview-analytics.md"
374        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
375
376        See also: `Overview()` with parameter `details="analytics"`.
377        """
378
379        self.reportFile = "deals.md"
380        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
381
382        See also: `Deals()`.
383        """
384
385        self.withdrawalLimitsFile = "limits.md"
386        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
387
388        See also: `OverviewLimits()` and `RequestLimits()`.
389        """
390
391        self.userInfoFile = "user-info.md"
392        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
393
394        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
395        """
396
397        self.userAccountsFile = "accounts.md"
398        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
399
400        See also: `OverviewAccounts()`, `RequestAccounts()`.
401        """
402
403        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
404        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
405
406        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
407
408        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
409        """
410
411        self.iList = None  # init iList for raw instruments data
412        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
413        
414        See also: `Listing()`, `DumpInstruments()`.
415        """
416
417        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
418        if useCache:
419            if os.path.exists(self.iListDumpFile):
420                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
421                curTime = datetime.now(tzutc())
422
423                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
424                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
425
426                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
427
428                else:
429                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
430
431                    uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile)))
432                    uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
433
434            else:
435                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
436                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
437
438        else:
439            self.iList = self.Listing()  # request new raw instruments data from broker server
440            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
441
442        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
443        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
444
445        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
446        """

Main class init.

Parameters
  • token: Bearer token for Tinkoff Invest API. It can be set from environment variable TKS_API_TOKEN.
  • accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. Also, this variable can be set from environment variable TKS_ACCOUNT_ID.
  • useCache: use default cache file with raw data to use instead of iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then set useCache=False.
  • defaultCache: path to default cache file. dump.json by default.
version

Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.

Latest version: https://pypi.org/project/tksbrokerapi/

aliases

Some aliases instead official tickers.

See also: TKSEnums.TKS_TICKER_ALIASES

ticker

String with ticker, e.g. GOOGL. Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.

See also: SearchByTicker(), SearchInstruments().

figi

String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6.

See also: SearchByFIGI(), SearchInstruments().

depth

Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.

See also: GetCurrentPrices().

server

Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest

See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().

timeout

Server operations timeout in seconds. Default: 15.

See also: SendAPIRequest().

headers

Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.

See also: SendAPIRequest().

body

Request body which send to broker server. Default: None.

See also: SendAPIRequest().

historyFile

Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only pandas dataframe.

See also: History().

htmlHistoryFile

Full path to the html file where rendered candles chart stored. Default: index.html.

See also: ShowHistoryChart().

instrumentsFile

Filename where full available to user instruments list will be saved. Default: instruments.md.

See also: ShowInstrumentsInfo().

searchResultsFile

Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.

See also: SearchInstruments().

pricesFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: GetListOfPrices().

infoFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().

bondsXLSXFile

Filename where wider pandas dataframe with more information about bonds: main info, current prices, bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.

See also: ExtendBondsData().

calendarFile

Filename where bonds payment calendar will be saved. Default: calendar.md.

Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.

See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().

overviewFile

Filename where current portfolio, open trades and orders will be saved. Default: overview.md.

See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().

overviewDigestFile

Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.

See also: Overview() with parameter details="digest".

overviewPositionsFile

Filename where only open positions, without everything else will be saved. Default: overview-positions.md.

See also: Overview() with parameter details="positions".

overviewOrdersFile

Filename where open limits and stop orders will be saved. Default: overview-orders.md.

See also: Overview() with parameter details="orders".

overviewAnalyticsFile

Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.

See also: Overview() with parameter details="analytics".

reportFile

Filename where history of deals and trade statistics will be saved. Default: deals.md.

See also: Deals().

withdrawalLimitsFile

Filename where table of funds available for withdrawal will be saved. Default: limits.md.

See also: OverviewLimits() and RequestLimits().

userInfoFile

Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.

See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().

userAccountsFile

Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.

See also: OverviewAccounts(), RequestAccounts().

iListDumpFile

Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.

Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.

See also: DumpInstruments() and DumpInstrumentsAsXLSX().

iList

Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.

See also: Listing(), DumpInstruments().

priceModel

PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.

See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator

def SendAPIRequest( self, url: str, reqType: str = 'GET', retry: int = 3, pause: int = 5, debug: bool = False) -> dict:
470    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict:
471        """
472        Send GET or POST request to broker server and receive JSON object.
473
474        self.header: must be defining with dictionary of headers.
475        self.body: if define then used as request body. None by default.
476        self.timeout: global request timeout, 15 seconds by default.
477        :param url: url with REST request.
478        :param reqType: send "GET" or "POST" request. "GET" by default.
479        :param retry: how many times retry after first request if an 5xx server errors occurred.
480        :param pause: sleep time in seconds between retries.
481        :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc.
482        :return: response JSON (dictionary) from broker.
483        """
484        if reqType not in ("GET", "POST"):
485            uLogger.error("You can define request type: 'GET' or 'POST'!")
486            raise Exception("Incorrect value")
487
488        if debug:
489            uLogger.debug("Request parameters:")
490            uLogger.debug("    - REST API URL: {}".format(url))
491            uLogger.debug("    - request type: {}".format(reqType))
492            uLogger.debug("    - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***")))
493            uLogger.debug("    - body: {}".format(self.body))
494
495        # fast hack to avoid all operations with some tickers/FIGI
496        responseJSON = {}
497        oK = True
498        for item in self.exclude:
499            if item in url:
500                if debug:
501                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
502
503                oK = False
504                break
505
506        if oK:
507            counter = 0
508            response = None
509            errMsg = ""
510
511            while not response and counter <= retry:
512                if reqType == "GET":
513                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
514
515                if reqType == "POST":
516                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
517
518                if debug:
519                    uLogger.debug("Response:")
520                    uLogger.debug("    - status code: {}".format(response.status_code))
521                    uLogger.debug("    - reason: {}".format(response.reason))
522                    uLogger.debug("    - body length: {}".format(len(response.text)))
523                    uLogger.debug("    - headers: {}".format(response.headers))
524
525                # Server returns some headers:
526                # - `x-ratelimit-limit` - shows the settings of the current user limit for this method.
527                # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute.
528                # - `x-ratelimit-reset` - time in seconds before resetting the request counter.
529                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
530                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
531                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
532                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
533                    sleep(rateLimitWait)
534
535                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
536                if 400 <= response.status_code < 500:
537                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
538                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
539                    counter = retry + 1
540
541                if 500 <= response.status_code < 600:
542                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
543                    uLogger.debug("    - not oK, {}".format(errMsg))
544                    counter += 1
545
546                    if counter <= retry:
547                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
548                        sleep(pause)
549
550            responseJSON = self._ParseJSON(response.text)
551
552            if errMsg:
553                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
554                uLogger.error("    - not oK, {}".format(errMsg))
555
556        return responseJSON

Send GET or POST request to broker server and receive JSON object.

self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.

Parameters
  • url: url with REST request.
  • reqType: send "GET" or "POST" request. "GET" by default.
  • retry: how many times retry after first request if an 5xx server errors occurred.
  • pause: sleep time in seconds between retries.
  • debug: if True then print more debug information, e.g. request and response parameters, headers etc.
Returns

response JSON (dictionary) from broker.

def Listing(self) -> dict:
589    def Listing(self) -> dict:
590        """
591        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
592
593        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
594        """
595        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
596        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
597
598        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
599        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
600        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
601
602        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
603        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
604        poolUpdater.close()
605
606        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
607        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
608        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
609
610        # calculate minimum price increment (step) for all instruments and set up instrument's type:
611        for iType in iList.keys():
612            for ticker in iList[iType]:
613                iList[iType][ticker]["type"] = iType
614
615                if "minPriceIncrement" in iList[iType][ticker].keys():
616                    iList[iType][ticker]["step"] = NanoToFloat(
617                        iList[iType][ticker]["minPriceIncrement"]["units"],
618                        iList[iType][ticker]["minPriceIncrement"]["nano"],
619                    )
620
621                else:
622                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
623
624        return iList

Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.

Returns

Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.

def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
626    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
627        """
628        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
629
630        See also: `DumpInstruments()`, `Listing()`.
631
632        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
633                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
634        """
635        if self.iListDumpFile is None or not self.iListDumpFile:
636            uLogger.error("Output name of dump file must be defined!")
637            raise Exception("Filename required")
638
639        if not self.iList or forceUpdate:
640            self.iList = self.Listing()
641
642        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
643
644        # Save as XLSX with separated sheets for every type of instruments:
645        with pd.ExcelWriter(
646                path=xlsxDumpFile,
647                date_format=TKS_DATE_FORMAT,
648                datetime_format=TKS_DATE_TIME_FORMAT,
649                mode="w",
650        ) as writer:
651            for iType in TKS_INSTRUMENTS:
652                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
653                df = df[sorted(df)]  # sorted by column names
654                df = df.applymap(
655                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
656                    na_action="ignore",
657                )  # converting numbers from nano-type to float in every cell
658                df.to_excel(
659                    writer,
660                    sheet_name=iType,
661                    encoding="UTF-8",
662                    freeze_panes=(1, 1),
663                )  # saving as XLSX-file with freeze first row and column as headers
664
665        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))

Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.

See also: DumpInstruments(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as XLSX-file (default: dump.xlsx) .
def DumpInstruments(self, forceUpdate: bool = True) -> str:
667    def DumpInstruments(self, forceUpdate: bool = True) -> str:
668        """
669        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
670        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
671
672        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
673
674        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
675                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
676        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
677        """
678        if self.iListDumpFile is None or not self.iListDumpFile:
679            uLogger.error("Output name of dump file must be defined!")
680            raise Exception("Filename required")
681
682        if not self.iList or forceUpdate:
683            self.iList = self.Listing()
684
685        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
686        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
687            fH.write(jsonDump)
688
689        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
690
691        return jsonDump

Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server using Listing() method. If iListDumpFile string is not empty then also save information to this file.

See also: DumpInstrumentsAsXLSX(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as JSON-file (default: dump.json).
Returns

serialized JSON formatted str with full data of instruments, also saved to the --output JSON-file.

def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
693    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
694        """
695        Show information about one instrument defined by json data and prints it in Markdown format.
696
697        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
698
699        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
700        :param show: if `True` then also printing information about instrument and its current price.
701        :return: multilines text in Markdown format with information about one instrument.
702        """
703        splitLine = "|                                                             |                                                        |\n"
704        infoText = ""
705
706        if iJSON is not None and iJSON and isinstance(iJSON, dict):
707            info = [
708                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
709                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
710                "| Parameters                                                  | Values                                                 |\n",
711                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
712                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
713                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
714            ]
715
716            if "sector" in iJSON.keys() and iJSON["sector"]:
717                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
718
719            info.append("| Country of instrument:                                      | {:<54} |\n".format("{}{}".format(
720                "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "",
721                iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "",
722            )))
723
724            info.extend([
725                splitLine,
726                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
727                "| Exchange:                                                   | {:<54} |\n".format(iJSON["exchange"]),
728            ])
729
730            if "isin" in iJSON.keys() and iJSON["isin"]:
731                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
732
733            if "classCode" in iJSON.keys():
734                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
735
736            info.extend([
737                splitLine,
738                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
739                splitLine,
740                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
741                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
742                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
743            ])
744
745            if iJSON["figi"]:
746                self.figi = iJSON["figi"]
747                iJSON = iJSON | self.RequestTradingStatus()
748
749                info.extend([
750                    splitLine,
751                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
752                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
753                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
754                ])
755
756            info.append(splitLine)
757
758            if "type" in iJSON.keys() and iJSON["type"]:
759                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
760
761            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
762                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
763
764            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
765                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
766
767            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
768                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
769
770            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
771                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
772
773            if "focusType" in iJSON.keys() and iJSON["focusType"]:
774                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
775
776            if "assetType" in iJSON.keys() and iJSON["assetType"]:
777                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
778
779            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
780                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
781
782            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
783                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
784
785            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
786                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
787
788            if "currency" in iJSON.keys():
789                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
790
791            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
792                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
793
794            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
795                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
796
797            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
798                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
799
800            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
801                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
802
803            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
804                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
805
806            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
807                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
808
809            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
810                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
811
812            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
813                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
814
815            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
816                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
817
818            iExt = None
819            if iJSON["type"] == "Bonds":
820                info.extend([
821                    splitLine,
822                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
823                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
824                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
825                        iJSON["nominal"]["currency"],
826                    )),
827                ])
828
829                if "floatingCouponFlag" in iJSON.keys():
830                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
831
832                if "amortizationFlag" in iJSON.keys():
833                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
834
835                info.append(splitLine)
836
837                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
838                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
839
840                iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
841
842                info.extend([
843                    "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
844                    "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
845                    "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
846                ])
847
848                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
849                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
850                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
851                        iJSON["aciValue"]["currency"]
852                    )))
853
854            if "currentPrice" in iJSON.keys():
855                info.append(splitLine)
856
857                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
858                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
859
860                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
861                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
862                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
863                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
864                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
865
866                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
867                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
868
869                info.extend([
870                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
871                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
872                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
873                    )),
874                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
875                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
876                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
877                    )),
878                    "| Changes between last deal price and last close              | {:<54} |\n".format(
879                        "{:.2f}%{}".format(
880                            iJSON["currentPrice"]["changes"],
881                            " ({}{:.2f} {})".format(
882                                "+" if bondChangesDelta > 0 else "",
883                                bondChangesDelta,
884                                aciCurrency
885                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
886                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
887                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
888                                currency
889                            ),
890                        )
891                    ),
892                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
893                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
894                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
895                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
896                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
897                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
898                    )),
899                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
900                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
901                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
902                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
903                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
904                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
905                    )),
906                ])
907
908            if "lot" in iJSON.keys():
909                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
910
911            if "step" in iJSON.keys() and iJSON["step"] != 0:
912                info.append("| Minimum price increment (step):                             | {:<54} |\n".format(iJSON["step"]))
913
914            # Add bond payment calendar:
915            if iJSON["type"] == "Bonds":
916                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
917                info.extend(["\n", strCalendar])
918
919            infoText += "".join(info)
920
921            if show:
922                uLogger.info("{}".format(infoText))
923
924            else:
925                uLogger.debug("{}".format(infoText))
926
927            if self.infoFile is not None:
928                with open(self.infoFile, "w", encoding="UTF-8") as fH:
929                    fH.write(infoText)
930
931                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
932
933        return infoText

Show information about one instrument defined by json data and prints it in Markdown format.

See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().

Parameters
  • iJSON: json data of instrument, example: iJSON = self.iList["Shares"][self.ticker]
  • show: if True then also printing information about instrument and its current price.
Returns

multilines text in Markdown format with information about one instrument.

def SearchByTicker( self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
 935    def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
 936        """
 937        Search and return raw broker's information about instrument by its ticker.
 938        `ticker` must be defined! If debug=True then print all debug messages.
 939
 940        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 941        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 942        :param debug: if `True` then print all debug console messages.
 943        :return: JSON formatted data with information about instrument.
 944        """
 945        tickerJSON = {}
 946        if debug:
 947            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 948
 949        if not self.ticker:
 950            uLogger.warning("self.ticker variable is not be empty!")
 951
 952        else:
 953            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 954                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 955                raise Exception("Instrument not allowed")
 956
 957            if not self.iList:
 958                self.iList = self.Listing()
 959
 960            if self.ticker in self.iList["Shares"].keys():
 961                tickerJSON = self.iList["Shares"][self.ticker]
 962                if debug:
 963                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 964
 965            elif self.ticker in self.iList["Currencies"].keys():
 966                tickerJSON = self.iList["Currencies"][self.ticker]
 967                if debug:
 968                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 969
 970            elif self.ticker in self.iList["Bonds"].keys():
 971                tickerJSON = self.iList["Bonds"][self.ticker]
 972                if debug:
 973                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 974
 975            elif self.ticker in self.iList["Etfs"].keys():
 976                tickerJSON = self.iList["Etfs"][self.ticker]
 977                if debug:
 978                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 979
 980            elif self.ticker in self.iList["Futures"].keys():
 981                tickerJSON = self.iList["Futures"][self.ticker]
 982                if debug:
 983                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 984
 985        if tickerJSON:
 986            self.figi = tickerJSON["figi"]
 987
 988            if requestPrice:
 989                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 990
 991                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 992                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 993
 994                else:
 995                    tickerJSON["currentPrice"]["changes"] = 0
 996
 997            if show:
 998                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 999
1000        else:
1001            if show:
1002                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
1003
1004        return tickerJSON

Search and return raw broker's information about instrument by its ticker. ticker must be defined! If debug=True then print all debug messages.

Parameters
  • requestPrice: if False then do not request current price of instrument (because this is long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
  • debug: if True then print all debug console messages.
Returns

JSON formatted data with information about instrument.

def SearchByFIGI( self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
1006    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
1007        """
1008        Search and return raw broker's information about instrument by its FIGI.
1009        `figi` must be defined! If debug=True then print all debug messages.
1010
1011        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1012        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1013        :param debug: if `True` then print all debug console messages.
1014        :return: JSON formatted data with information about instrument.
1015        """
1016        figiJSON = {}
1017        if debug:
1018            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
1019
1020        if not self.figi:
1021            uLogger.warning("self.figi variable is not be empty!")
1022
1023        else:
1024            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1025                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
1026                raise Exception("Instrument not allowed")
1027
1028            if not self.iList:
1029                self.iList = self.Listing()
1030
1031            for item in self.iList["Shares"].keys():
1032                if self.figi == self.iList["Shares"][item]["figi"]:
1033                    figiJSON = self.iList["Shares"][item]
1034
1035                    if debug:
1036                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
1037
1038                    break
1039
1040            if not figiJSON:
1041                for item in self.iList["Currencies"].keys():
1042                    if self.figi == self.iList["Currencies"][item]["figi"]:
1043                        figiJSON = self.iList["Currencies"][item]
1044
1045                        if debug:
1046                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1047
1048                        break
1049
1050            if not figiJSON:
1051                for item in self.iList["Bonds"].keys():
1052                    if self.figi == self.iList["Bonds"][item]["figi"]:
1053                        figiJSON = self.iList["Bonds"][item]
1054
1055                        if debug:
1056                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1057
1058                        break
1059
1060            if not figiJSON:
1061                for item in self.iList["Etfs"].keys():
1062                    if self.figi == self.iList["Etfs"][item]["figi"]:
1063                        figiJSON = self.iList["Etfs"][item]
1064
1065                        if debug:
1066                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1067
1068                        break
1069
1070            if not figiJSON:
1071                for item in self.iList["Futures"].keys():
1072                    if self.figi == self.iList["Futures"][item]["figi"]:
1073                        figiJSON = self.iList["Futures"][item]
1074
1075                        if debug:
1076                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1077
1078                        break
1079
1080        if figiJSON:
1081            self.figi = figiJSON["figi"]
1082            self.ticker = figiJSON["ticker"]
1083
1084            if requestPrice:
1085                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1086
1087                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1088                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1089
1090                else:
1091                    figiJSON["currentPrice"]["changes"] = 0
1092
1093            if show:
1094                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1095
1096        else:
1097            if show:
1098                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1099
1100        return figiJSON

Search and return raw broker's information about instrument by its FIGI. figi must be defined! If debug=True then print all debug messages.

Parameters
  • requestPrice: if False then do not request current price of instrument (it's long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
  • debug: if True then print all debug console messages.
Returns

JSON formatted data with information about instrument.

def GetCurrentPrices(self, show: bool = True) -> dict:
1102    def GetCurrentPrices(self, show: bool = True) -> dict:
1103        """
1104        Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record:
1105        `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1106
1107        See also:
1108
1109        :param show: if `True` then print DOM to log and console.
1110        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1111        """
1112        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1113
1114        if self.depth < 1:
1115            uLogger.error("Depth of Market (DOM) must be >=1!")
1116            raise Exception("Incorrect value")
1117
1118        if not (self.ticker or self.figi):
1119            uLogger.error("self.ticker or self.figi variables must be defined!")
1120            raise Exception("Ticker or FIGI required")
1121
1122        if self.ticker and not self.figi:
1123            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1124            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1125
1126        if not self.ticker and self.figi:
1127            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1128            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1129
1130        if not self.figi:
1131            uLogger.error("FIGI is not defined!")
1132            raise Exception("Ticker or FIGI required")
1133
1134        else:
1135            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1136
1137            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1138            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1139            self.body = str({"figi": self.figi, "depth": self.depth})
1140            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")
1141
1142            if pricesResponse:
1143                # list of dicts with sellers orders:
1144                prices["buy"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1145
1146                # list of dicts with buyers orders:
1147                prices["sell"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1148
1149                # max price of instrument at this time:
1150                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1151
1152                # min price of instrument at this time:
1153                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1154
1155                # last price of deal with instrument:
1156                prices["lastPrice"] = NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]) if "lastPrice" in pricesResponse.keys() else 0
1157
1158                # last close price of instrument:
1159                prices["closePrice"] = NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]) if "closePrice" in pricesResponse.keys() else 0
1160
1161            else:
1162                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1163                uLogger.debug("Server response: {}".format(pricesResponse))
1164
1165            if show:
1166                if prices["buy"] or prices["sell"]:
1167                    info = [
1168                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1169                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1170                            self.ticker,
1171                            self.figi,
1172                            self.depth,
1173                        ),
1174                        uLog.sepShort, "\n",
1175                        " Orders of Buyers   | Orders of Sellers\n",
1176                        uLog.sepShort, "\n",
1177                        " Sell prices (vol.) | Buy prices (vol.)\n",
1178                        uLog.sepShort, "\n",
1179                    ]
1180
1181                    if not prices["buy"]:
1182                        info.append("                    | No orders!\n")
1183                        sumBuy = 0
1184
1185                    else:
1186                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1187                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1188                        for item in maxMinSorted:
1189                            info.append("                    | {} ({})\n".format(item["price"], item["quantity"]))
1190
1191                    if not prices["sell"]:
1192                        info.append("No orders!          |\n")
1193                        sumSell = 0
1194
1195                    else:
1196                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1197                        for item in prices["sell"]:
1198                            info.append("{:>19} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1199
1200                    info.extend([
1201                        uLog.sepShort, "\n",
1202                        "{:>19} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1203                        uLog.sepShort, "\n",
1204                    ])
1205
1206                    infoText = "".join(info)
1207
1208                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1209
1210                else:
1211                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1212
1213        return prices

Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record: {"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.

See also:

Parameters
  • show: if True then print DOM to log and console.
Returns

orders book dict with lists of current buy and sell prices: {"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}.

def ShowInstrumentsInfo(self, show: bool = True) -> str:
1215    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1216        """
1217        This method get and show information about all available broker instruments for current user account.
1218        If `instrumentsFile` string is not empty then also save information to this file.
1219
1220        :param show: if `True` then print results to console, if `False` - print only to file.
1221        :return: multi-lines string with all available broker instruments
1222        """
1223        if not self.iList:
1224            self.iList = self.Listing()
1225
1226        info = [
1227            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1228            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1229        ]
1230
1231        # add instruments count by type:
1232        for iType in self.iList.keys():
1233            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1234
1235        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1236        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1237
1238        # generating info tables with all instruments by type:
1239        for iType in self.iList.keys():
1240            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1241
1242            for instrument in self.iList[iType].keys():
1243                iName = self.iList[iType][instrument]["name"]  # instrument's name
1244                if len(iName) > 57:
1245                    iName = "{}...".format(iName[:54])  # right trim for a long string
1246
1247                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1248                    self.iList[iType][instrument]["ticker"],
1249                    iName,
1250                    self.iList[iType][instrument]["figi"],
1251                    self.iList[iType][instrument]["currency"],
1252                    self.iList[iType][instrument]["lot"],
1253                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1254                ))
1255
1256        infoText = "".join(info)
1257
1258        if show:
1259            uLogger.info(infoText)
1260
1261        if self.instrumentsFile:
1262            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1263                fH.write(infoText)
1264
1265            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1266
1267        return infoText

This method get and show information about all available broker instruments for current user account. If instrumentsFile string is not empty then also save information to this file.

Parameters
  • show: if True then print results to console, if False - print only to file.
Returns

multi-lines string with all available broker instruments

def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1269    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1270        """
1271        This method search and show information about instruments by part of its ticker, FIGI or name.
1272        If `searchResultsFile` string is not empty then also save information to this file.
1273
1274        :param pattern: string with part of ticker, FIGI or instrument's name.
1275        :param show: if `True` then print results to console, if `False` - return list of result only.
1276        :return: list of dictionaries with all found instruments.
1277        """
1278        if not self.iList:
1279            self.iList = self.Listing()
1280
1281        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1282        compiledPattern = re.compile(pattern, re.IGNORECASE)
1283
1284        for iType in self.iList:
1285            for instrument in self.iList[iType].values():
1286                searchResult = compiledPattern.search(" ".join(
1287                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1288                ))
1289
1290                if searchResult:
1291                    searchResults[iType][instrument["ticker"]] = instrument
1292
1293        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1294        info = [
1295            "# Search results\n\n",
1296            "* **Search pattern:** [{}]\n".format(pattern),
1297            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1298            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1299        ]
1300        infoShort = info[:]
1301
1302        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1303        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1304        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1305
1306        if resultsLen == 0:
1307            info.append("\nNo results\n")
1308            infoShort.append("\nNo results\n")
1309            uLogger.warning("No results. Try changing your search pattern.")
1310
1311        else:
1312            for iType in searchResults:
1313                iTypeValuesCount = len(searchResults[iType].values())
1314                if iTypeValuesCount > 0:
1315                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1316                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1317
1318                    for instrument in searchResults[iType].values():
1319                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1320                            instrument["type"],
1321                            instrument["ticker"],
1322                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1323                            instrument["figi"],
1324                        ))
1325
1326                    if iTypeValuesCount <= 5:
1327                        infoShort.extend(info[-iTypeValuesCount:])
1328
1329                    else:
1330                        infoShort.extend(info[-5:])
1331                        infoShort.append(skippedLine)
1332
1333        infoText = "".join(info)
1334        infoTextShort = "".join(infoShort)
1335
1336        if show:
1337            uLogger.info(infoTextShort)
1338            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1339
1340        if self.searchResultsFile:
1341            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1342                fH.write(infoText)
1343
1344            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1345
1346        return searchResults

This method search and show information about instruments by part of its ticker, FIGI or name. If searchResultsFile string is not empty then also save information to this file.

Parameters
  • pattern: string with part of ticker, FIGI or instrument's name.
  • show: if True then print results to console, if False - return list of result only.
Returns

list of dictionaries with all found instruments.

def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1348    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1349        """
1350        Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
1351
1352        :param instruments: list of strings with tickers or FIGIs.
1353        :return: list with unique instrument FIGIs only.
1354        """
1355        requestedInstruments = []
1356        for iName in instruments:
1357            if iName not in self.aliases.keys():
1358                if iName not in requestedInstruments:
1359                    requestedInstruments.append(iName)
1360
1361            else:
1362                if iName not in requestedInstruments:
1363                    if self.aliases[iName] not in requestedInstruments:
1364                        requestedInstruments.append(self.aliases[iName])
1365
1366        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1367
1368        onlyUniqueFIGIs = []
1369        for iName in requestedInstruments:
1370            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1371                continue
1372
1373            self.ticker = iName
1374            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1375
1376            if not iData:
1377                self.ticker = ""
1378                self.figi = iName
1379
1380                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1381
1382                if not iData:
1383                    self.figi = ""
1384                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1385
1386            if iData and iData["figi"] not in onlyUniqueFIGIs:
1387                onlyUniqueFIGIs.append(iData["figi"])
1388
1389        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1390
1391        return onlyUniqueFIGIs

Creating list with unique instrument FIGIs from input list of tickers or FIGIs.

Parameters
  • instruments: list of strings with tickers or FIGIs.
Returns

list with unique instrument FIGIs only.

def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1393    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1394        """
1395        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1396        See limits: https://tinkoff.github.io/investAPI/limits/
1397        If `pricesFile` string is not empty then also save information to this file.
1398
1399        :param instruments: list of strings with tickers or FIGIs.
1400        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1401        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1402                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1403        """
1404        if instruments is None or not instruments:
1405            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1406            raise Exception("Ticker or FIGI required")
1407
1408        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1409
1410        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1411
1412        iList = []  # trying to get info and current prices about all unique instruments:
1413        for self.figi in onlyUniqueFIGIs:
1414            iData = self.SearchByFIGI(requestPrice=True)
1415            iList.append(iData)
1416
1417        self.ShowListOfPrices(iList, show)
1418
1419        return iList

This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! See limits: https://tinkoff.github.io/investAPI/limits/ If pricesFile string is not empty then also save information to this file.

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • show: if True then prints prices to console, if False - prints only to file pricesFile.
Returns

list of instruments looks like [{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker() or SearchByFIGI() methods.

def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1421    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1422        """
1423        Show table contains current prices of given instruments.
1424
1425        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1426                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1427        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1428        :return: multilines text in Markdown format as a table contains current prices.
1429        """
1430        infoText = ""
1431
1432        if show or self.pricesFile:
1433            info = [
1434                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1435                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1436                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1437            ]
1438
1439            for item in iList:
1440                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1441                    item["ticker"],
1442                    item["figi"],
1443                    item["type"],
1444                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1445                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1446                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1447                    "{} / {}".format(
1448                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1449                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1450                    ),
1451                    "{} / {}".format(
1452                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1453                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1454                    ),
1455                    item["currency"],
1456                ))
1457
1458            infoText = "".join(info)
1459
1460            if show:
1461                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1462
1463            if self.pricesFile:
1464                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1465                    fH.write(infoText)
1466
1467                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1468
1469        return infoText

Show table contains current prices of given instruments.

Parameters
  • **iList: list of instruments looks like [{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker(requestPrice=True) or by SearchByFIGI(requestPrice=True) methods.
  • show: if True then prints prices to console, if False - prints only to file pricesFile.
Returns

multilines text in Markdown format as a table contains current prices.

def RequestTradingStatus(self) -> dict:
1471    def RequestTradingStatus(self) -> dict:
1472        """
1473        Requesting trading status for the instrument defined by `figi` variable.
1474        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1475        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1476
1477        :return: dictionary with trading status attributes. Response example:
1478                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1479                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1480        """
1481        if self.figi is None or not self.figi:
1482            uLogger.error("Variable `figi` must be defined for using this method!")
1483            raise Exception("FIGI required")
1484
1485        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1486
1487        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1488        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1489        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1490
1491        uLogger.debug("Records about current trading status successfully received")
1492
1493        return tradingStatus

Requesting trading status for the instrument defined by figi variable. REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest

Returns

dictionary with trading status attributes. Response example: {"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}

def RequestPortfolio(self) -> dict:
1495    def RequestPortfolio(self) -> dict:
1496        """
1497        Requesting actual user's portfolio for current `accountId`.
1498        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1499        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1500
1501        :return: dictionary with user's portfolio.
1502        """
1503        if self.accountId is None or not self.accountId:
1504            uLogger.error("Variable `accountId` must be defined for using this method!")
1505            raise Exception("Account ID required")
1506
1507        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1508
1509        self.body = str({"accountId": self.accountId})
1510        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1511        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1512
1513        uLogger.debug("Records about user's portfolio successfully received")
1514
1515        return rawPortfolio

Requesting actual user's portfolio for current accountId. REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest

Returns

dictionary with user's portfolio.

def RequestPositions(self) -> dict:
1517    def RequestPositions(self) -> dict:
1518        """
1519        Requesting open positions by currencies and instruments for current `accountId`.
1520        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1521        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1522
1523        :return: dictionary with open positions by instruments.
1524        """
1525        if self.accountId is None or not self.accountId:
1526            uLogger.error("Variable `accountId` must be defined for using this method!")
1527            raise Exception("Account ID required")
1528
1529        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1530
1531        self.body = str({"accountId": self.accountId})
1532        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1533        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1534
1535        uLogger.debug("Records about current open positions successfully received")
1536
1537        return rawPositions

Requesting open positions by currencies and instruments for current accountId. REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest

Returns

dictionary with open positions by instruments.

def RequestPendingOrders(self) -> list:
1539    def RequestPendingOrders(self) -> list:
1540        """
1541        Requesting current actual pending orders for current `accountId`.
1542        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1543        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1544
1545        :return: list of dictionaries with pending orders.
1546        """
1547        if self.accountId is None or not self.accountId:
1548            uLogger.error("Variable `accountId` must be defined for using this method!")
1549            raise Exception("Account ID required")
1550
1551        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1552
1553        self.body = str({"accountId": self.accountId})
1554        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1555        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1556
1557        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1558
1559        return rawOrders

Requesting current actual pending orders for current accountId. REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest

Returns

list of dictionaries with pending orders.

def RequestStopOrders(self) -> list:
1561    def RequestStopOrders(self) -> list:
1562        """
1563        Requesting current actual stop orders for current `accountId`.
1564        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1565        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1566
1567        :return: list of dictionaries with stop orders.
1568        """
1569        if self.accountId is None or not self.accountId:
1570            uLogger.error("Variable `accountId` must be defined for using this method!")
1571            raise Exception("Account ID required")
1572
1573        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1574
1575        self.body = str({"accountId": self.accountId})
1576        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1577        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1578
1579        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1580
1581        return rawStopOrders

Requesting current actual stop orders for current accountId. REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest

Returns

list of dictionaries with stop orders.

def Overview(self, show: bool = False, details: str = 'full') -> dict:
1583    def Overview(self, show: bool = False, details: str = "full") -> dict:
1584        """
1585        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1586        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1587        are defined then also save information to file.
1588
1589        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1590        many requests about the state of the portfolio, and then, based on the received data, a large number
1591        of calculation and statistics are collected.
1592
1593        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1594        :param details: how detailed should the information be? You should specify one of strings:
1595                        `full` - shows full available information about portfolio status (by default),
1596                        `positions` - shows only open positions,
1597                        `digest` - show a short digest of the portfolio status,
1598                        `analytics` - shows only the analytics section and the distribution of the portfolio by various categories,
1599                        `orders` - shows only sections of open limits and stop orders.
1600        :return: dictionary with client's raw portfolio and some statistics.
1601        """
1602        if self.accountId is None or not self.accountId:
1603            uLogger.error("Variable `accountId` must be defined for using this method!")
1604            raise Exception("Account ID required")
1605
1606        view = {
1607            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1608                "headers": {},  # list of dictionaries, response headers without "positions" section
1609                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1610                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1611                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1612                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1613                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1614                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1615                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1616                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1617                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1618            },
1619            "stat": {  # --- some statistics calculated using "raw" sections:
1620                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1621                "availableRUB": 0.,  # available rubles (without other currencies)
1622                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1623                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1624                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1625                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1626                "sharesCostRUB": 0.,  # costs of all shares in RUB
1627                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1628                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1629                "futuresCostRUB": 0.,  # costs of all futures in RUB
1630                "Currencies": [],  # list of dictionaries of all currencies statistics
1631                "Shares": [],  # list of dictionaries of all shares statistics
1632                "Bonds": [],  # list of dictionaries of all bonds statistics
1633                "Etfs": [],  # list of dictionaries of all etfs statistics
1634                "Futures": [],  # list of dictionaries of all futures statistics
1635                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1636                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1637                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1638                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1639                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1640            },
1641            "analytics": {  # --- some analytics of portfolio:
1642                "distrByAssets": {},  # portfolio distribution by assets
1643                "distrByCompanies": {},  # portfolio distribution by companies
1644                "distrBySectors": {},  # portfolio distribution by sectors
1645                "distrByCurrencies": {},  # portfolio distribution by currencies
1646                "distrByCountries": {},  # portfolio distribution by countries
1647            }
1648        }
1649
1650        details = details.lower()
1651        availableDetails = ["full", "positions", "digest", "analytics", "orders"]
1652        if details not in availableDetails:
1653            details = "full"
1654            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1655
1656        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1657
1658        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1659        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1660        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1661        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1662
1663        # save response headers without "positions" section:
1664        for key in portfolioResponse.keys():
1665            if key != "positions":
1666                view["raw"]["headers"][key] = portfolioResponse[key]
1667
1668            else:
1669                continue
1670
1671        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1672        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1673        for item in portfolioResponse["positions"]:
1674            if item["instrumentType"] == "currency":
1675                self.figi = item["figi"]
1676                curr = self.SearchByFIGI(requestPrice=False)
1677
1678                # current price of currency in RUB:
1679                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1680                    "name": curr["name"],
1681                    "currentPrice": NanoToFloat(
1682                        item["currentPrice"]["units"],
1683                        item["currentPrice"]["nano"]
1684                    ),
1685                }
1686
1687                view["raw"]["Currencies"].append(item)
1688
1689            elif item["instrumentType"] == "share":
1690                view["raw"]["Shares"].append(item)
1691
1692            elif item["instrumentType"] == "bond":
1693                view["raw"]["Bonds"].append(item)
1694
1695            elif item["instrumentType"] == "etf":
1696                view["raw"]["Etfs"].append(item)
1697
1698            elif item["instrumentType"] == "futures":
1699                view["raw"]["Futures"].append(item)
1700
1701            else:
1702                continue
1703
1704        # how many volume of currencies (by ISO currency name) are blocked:
1705        for item in view["raw"]["positions"]["blocked"]:
1706            blocked = NanoToFloat(item["units"], item["nano"])
1707            if blocked > 0:
1708                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1709
1710        # how many volume of instruments (by FIGI) are blocked:
1711        for item in view["raw"]["positions"]["securities"]:
1712            blocked = int(item["blocked"])
1713            if blocked > 0:
1714                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1715
1716        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1717
1718        if "rub" in allBlocked.keys():
1719            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1720
1721        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1722        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1723        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1724        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1725        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1726        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1727        view["stat"]["portfolioCostRUB"] = sum([
1728            view["stat"]["allCurrenciesCostRUB"],
1729            view["stat"]["sharesCostRUB"],
1730            view["stat"]["bondsCostRUB"],
1731            view["stat"]["etfsCostRUB"],
1732            view["stat"]["futuresCostRUB"],
1733        ])
1734
1735        # --- calculating some portfolio statistics:
1736        byComp = {}  # distribution by companies
1737        bySect = {}  # distribution by sectors
1738        byCurr = {}  # distribution by currencies (include RUB)
1739        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1740        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1741
1742        for item in portfolioResponse["positions"]:
1743            self.figi = item["figi"]
1744            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1745
1746            if instrument:
1747                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1748                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1749
1750                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1751                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1752
1753                else:
1754                    blocked = 0
1755
1756                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1757                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1758                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1759                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1760                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1761                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1762                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1763                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1764                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1765                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1766                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1767                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1768
1769                statData = {
1770                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1771                    "ticker": instrument["ticker"],  # ticker by FIGI
1772                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1773                    "volume": volume,  # available volume of instrument
1774                    "lots": lots,  # volume in lots of instrument
1775                    "direction": direction,  # direction of an instrument's position: short or long
1776                    "blocked": blocked,  # blocked volume of currency or instrument
1777                    "currentPrice": curPrice,  # current instrument's price in basic asset
1778                    "average": average,  # current average position price
1779                    "cost": cost,  # current cost of all volume of instrument in basic asset
1780                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1781                    "costRUB": costRUB,  # cost of instrument in ruble
1782                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1783                    "profit": profit,  # expected profit at current moment
1784                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1785                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1786                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1787                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1788                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1789                    "step": instrument["step"],  # minimum price increment
1790                }
1791
1792                # adding distribution by unique countries:
1793                if statData["country"] not in byCountry.keys():
1794                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1795
1796                else:
1797                    byCountry[statData["country"]]["cost"] += costRUB
1798                    byCountry[statData["country"]]["percent"] += percentCostRUB
1799
1800                if item["instrumentType"] != "currency":
1801                    # adding distribution by unique companies:
1802                    if statData["name"]:
1803                        if statData["name"] not in byComp.keys():
1804                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1805
1806                        else:
1807                            byComp[statData["name"]]["cost"] += costRUB
1808                            byComp[statData["name"]]["percent"] += percentCostRUB
1809
1810                    # adding distribution by unique sectors:
1811                    if statData["sector"] not in bySect.keys():
1812                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1813
1814                    else:
1815                        bySect[statData["sector"]]["cost"] += costRUB
1816                        bySect[statData["sector"]]["percent"] += percentCostRUB
1817
1818                # adding distribution by unique currencies:
1819                if currency not in byCurr.keys():
1820                    byCurr[currency] = {
1821                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1822                        "cost": costRUB,
1823                        "percent": percentCostRUB
1824                    }
1825
1826                else:
1827                    byCurr[currency]["cost"] += costRUB
1828                    byCurr[currency]["percent"] += percentCostRUB
1829
1830                # saving statistics for every instrument:
1831                if item["instrumentType"] == "currency":
1832                    view["stat"]["Currencies"].append(statData)
1833
1834                    # update dict with free funds for trading (total - blocked) by currencies
1835                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1836                    view["stat"]["funds"][currency] = {
1837                        "total": volume,
1838                        "totalCostRUB": costRUB,  # total volume cost in rubles
1839                        "free": volume - blocked,
1840                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1841                    }
1842
1843                elif item["instrumentType"] == "share":
1844                    view["stat"]["Shares"].append(statData)
1845
1846                elif item["instrumentType"] == "bond":
1847                    view["stat"]["Bonds"].append(statData)
1848
1849                elif item["instrumentType"] == "etf":
1850                    view["stat"]["Etfs"].append(statData)
1851
1852                elif item["instrumentType"] == "Futures":
1853                    view["stat"]["Futures"].append(statData)
1854
1855                else:
1856                    continue
1857
1858        # total changes in Russian Ruble:
1859        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1860        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1861        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1862        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1863        view["stat"]["funds"]["rub"] = {
1864            "total": view["stat"]["availableRUB"],
1865            "totalCostRUB": view["stat"]["availableRUB"],
1866            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1867            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1868        }
1869
1870        # --- pending orders sector data:
1871        uniquePendingOrders = []
1872        uniquePendingOrdersFIGIs = []
1873        for item in view["raw"]["orders"]:
1874            if item["figi"] not in uniquePendingOrdersFIGIs:
1875                uniquePendingOrdersFIGIs.append(item["figi"])
1876                uniquePendingOrders.append(item)
1877
1878        for item in uniquePendingOrders:
1879            self.figi = item["figi"]
1880            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1881
1882            if instrument:
1883                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1884                orderType = TKS_ORDER_TYPES[item["orderType"]]
1885                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1886                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1887
1888                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1889                if item["direction"] == "ORDER_DIRECTION_BUY":
1890                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1891
1892                else:
1893                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1894
1895                # requested price for order execution:
1896                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1897
1898                # necessary changes in percent to reach target from current price:
1899                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1900
1901                view["stat"]["orders"].append({
1902                    "orderID": item["orderId"],  # orderId number parameter of current order
1903                    "figi": item["figi"],  # FIGI identification
1904                    "ticker": instrument["ticker"],  # ticker name by FIGI
1905                    "lotsRequested": item["lotsRequested"],  # requested lots value
1906                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1907                    "currentPrice": lastPrice,  # current instrument's price for defined action
1908                    "targetPrice": target,  # requested price for order execution in base currency
1909                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1910                    "percentChanges": changes,  # changes in percent to target from current price
1911                    "currency": item["currency"],  # instrument's currency name
1912                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1913                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1914                    "status": orderState,  # order status from TKS_ORDER_STATES
1915                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1916                })
1917
1918        # --- stop orders sector data:
1919        uniqueStopOrders = []
1920        uniqueStopOrdersFIGIs = []
1921        for item in view["raw"]["stopOrders"]:
1922            if item["figi"] not in uniqueStopOrdersFIGIs:
1923                uniqueStopOrdersFIGIs.append(item["figi"])
1924                uniqueStopOrders.append(item)
1925
1926        for item in uniqueStopOrders:
1927            self.figi = item["figi"]
1928            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1929
1930            if instrument:
1931                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1932                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1933                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1934
1935                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1936                if "expirationTime" in item.keys():
1937                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1938                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1939
1940                else:
1941                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1942                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1943
1944                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1945                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1946                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1947
1948                else:
1949                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1950
1951                # requested price when stop-order executed:
1952                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1953
1954                # price for limit-order, set up when stop-order executed:
1955                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1956
1957                # necessary changes in percent to reach target from current price:
1958                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1959
1960                view["stat"]["stopOrders"].append({
1961                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1962                    "figi": item["figi"],  # FIGI identification
1963                    "ticker": instrument["ticker"],  # ticker name by FIGI
1964                    "lotsRequested": item["lotsRequested"],  # requested lots value
1965                    "currentPrice": lastPrice,  # current instrument's price for defined action
1966                    "targetPrice": target,  # requested price for stop-order execution in base currency
1967                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1968                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1969                    "percentChanges": changes,  # changes in percent to target from current price
1970                    "currency": item["currency"],  # instrument's currency name
1971                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1972                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1973                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1974                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1975                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1976                })
1977
1978        # --- calculating data for analytics section:
1979        # portfolio distribution by assets:
1980        view["analytics"]["distrByAssets"] = {
1981            "Ruble": {
1982                "uniques": 1,
1983                "cost": view["stat"]["availableRUB"],
1984                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1985            },
1986            "Currencies": {
1987                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
1988                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
1989                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1990            },
1991            "Shares": {
1992                "uniques": len(view["stat"]["Shares"]),
1993                "cost": view["stat"]["sharesCostRUB"],
1994                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1995            },
1996            "Bonds": {
1997                "uniques": len(view["stat"]["Bonds"]),
1998                "cost": view["stat"]["bondsCostRUB"],
1999                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2000            },
2001            "Etfs": {
2002                "uniques": len(view["stat"]["Etfs"]),
2003                "cost": view["stat"]["etfsCostRUB"],
2004                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2005            },
2006            "Futures": {
2007                "uniques": len(view["stat"]["Futures"]),
2008                "cost": view["stat"]["futuresCostRUB"],
2009                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2010            },
2011        }
2012
2013        # portfolio distribution by companies:
2014        view["analytics"]["distrByCompanies"]["All money cash"] = {
2015            "ticker": "",
2016            "cost": view["stat"]["allCurrenciesCostRUB"],
2017            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2018        }
2019        view["analytics"]["distrByCompanies"].update(byComp)
2020
2021        # portfolio distribution by sectors:
2022        view["analytics"]["distrBySectors"]["All money cash"] = {
2023            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2024            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2025        }
2026        view["analytics"]["distrBySectors"].update(bySect)
2027
2028        # portfolio distribution by currencies:
2029        view["analytics"]["distrByCurrencies"].update(byCurr)
2030        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2031        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2032
2033        # portfolio distribution by countries:
2034        view["analytics"]["distrByCountries"].update(byCountry)
2035
2036        # --- Prepare text statistics overview in human-readable:
2037        if show:
2038            # Whatever the value `details`, header not changes:
2039            info = [
2040                "# Client's portfolio\n\n",
2041                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2042                "* **Account ID:** [{}]\n".format(self.accountId),
2043            ]
2044
2045            if details in ["full", "positions", "digest"]:
2046                info.extend([
2047                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2048                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2049                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2050                        view["stat"]["totalChangesRUB"],
2051                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2052                        view["stat"]["totalChangesPercentRUB"],
2053                    ),
2054                ])
2055
2056            if details in ["full", "positions"]:
2057                info.extend([
2058                    "## Open positions\n\n",
2059                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2060                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2061                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2062                        "{:.2f} ({:.2f}) rub".format(
2063                            view["stat"]["availableRUB"],
2064                            view["stat"]["blockedRUB"],
2065                        )
2066                    )
2067                ])
2068
2069                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2070                    return [
2071                        "|                             |                                 |          |              |              |                     |                              |\n",
2072                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2073                            noTradeStr if noTradeStr else typeStr,
2074                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2075                        ),
2076                    ]
2077
2078                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2079                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2080                        "{} [{}]".format(data["ticker"], data["figi"]),
2081                        "{:.2f} ({:.2f}) {}".format(
2082                            data["volume"],
2083                            data["blocked"],
2084                            data["currency"],
2085                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2086                            data["volume"],
2087                            data["blocked"],
2088                        ),
2089                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2090                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2091                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2092                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2093                        "{}{:.2f} {} ({}{:.2f}%)".format(
2094                            "+" if data["profit"] > 0 else "",
2095                            data["profit"], data["baseCurrencyName"],
2096                            "+" if data["percentProfit"] > 0 else "",
2097                            data["percentProfit"],
2098                        ),
2099                    )
2100
2101                # --- Show currencies section:
2102                if view["stat"]["Currencies"]:
2103                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2104                    for item in view["stat"]["Currencies"]:
2105                        info.append(_InfoStr(item, showCurrencyName=True))
2106
2107                else:
2108                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2109
2110                # --- Show shares section:
2111                if view["stat"]["Shares"]:
2112                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2113
2114                    for item in view["stat"]["Shares"]:
2115                        info.append(_InfoStr(item))
2116
2117                else:
2118                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2119
2120                # --- Show bonds section:
2121                if view["stat"]["Bonds"]:
2122                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2123
2124                    for item in view["stat"]["Bonds"]:
2125                        info.append(_InfoStr(item))
2126
2127                else:
2128                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2129
2130                # --- Show etfs section:
2131                if view["stat"]["Etfs"]:
2132                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2133
2134                    for item in view["stat"]["Etfs"]:
2135                        info.append(_InfoStr(item))
2136
2137                else:
2138                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2139
2140                # --- Show futures section:
2141                if view["stat"]["Futures"]:
2142                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2143
2144                    for item in view["stat"]["Futures"]:
2145                        info.append(_InfoStr(item))
2146
2147                else:
2148                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2149
2150            if details in ["full", "orders"]:
2151                # --- Show pending orders section:
2152                if view["stat"]["orders"]:
2153                    info.extend([
2154                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2155                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2156                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2157                    ])
2158
2159                    for item in view["stat"]["orders"]:
2160                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2161                            "{} [{}]".format(item["ticker"], item["figi"]),
2162                            item["orderID"],
2163                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2164                            "{} {} ({}{:.2f}%)".format(
2165                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2166                                item["baseCurrencyName"],
2167                                "+" if item["percentChanges"] > 0 else "",
2168                                float(item["percentChanges"]),
2169                            ),
2170                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2171                            item["action"],
2172                            item["type"],
2173                            item["date"],
2174                        ))
2175
2176                else:
2177                    info.append("\n## Total pending limit-orders: 0\n")
2178
2179                # --- Show stop orders section:
2180                if view["stat"]["stopOrders"]:
2181                    info.extend([
2182                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2183                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2184                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2185                    ])
2186
2187                    for item in view["stat"]["stopOrders"]:
2188                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2189                            "{} [{}]".format(item["ticker"], item["figi"]),
2190                            item["orderID"],
2191                            item["lotsRequested"],
2192                            "{} {} ({}{:.2f}%)".format(
2193                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2194                                item["baseCurrencyName"],
2195                                "+" if item["percentChanges"] > 0 else "",
2196                                float(item["percentChanges"]),
2197                            ),
2198                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2199                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2200                            item["action"],
2201                            item["type"],
2202                            item["expType"],
2203                            item["createDate"],
2204                            item["expDate"],
2205                        ))
2206
2207                else:
2208                    info.append("\n## Total stop-orders: 0\n")
2209
2210            if details in ["full", "analytics"]:
2211                # -- Show analytics section:
2212                if view["stat"]["portfolioCostRUB"] > 0:
2213                    info.extend([
2214                        "\n# Analytics\n"
2215                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2216                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2217                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2218                            view["stat"]["totalChangesRUB"],
2219                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2220                            view["stat"]["totalChangesPercentRUB"],
2221                        ),
2222                        "\n## Portfolio distribution by assets\n"
2223                        "\n| Type       | Uniques | Percent | Current cost       |\n",
2224                        "|------------|---------|---------|--------------------|\n",
2225                    ])
2226
2227                    for key in view["analytics"]["distrByAssets"].keys():
2228                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2229                            info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format(
2230                                key,
2231                                view["analytics"]["distrByAssets"][key]["uniques"],
2232                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2233                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2234                            ))
2235
2236                    maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()])
2237                    info.extend([
2238                        "\n## Portfolio distribution by companies\n"
2239                        "\n| Company{} | Percent | Current cost       |\n".format(" " * (maxLenNames - 7)),
2240                        "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)),
2241                    ])
2242
2243                    for company in view["analytics"]["distrByCompanies"].keys():
2244                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2245                            nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"])
2246                            info.append("| {} | {:<7} | {:<18} |\n".format(
2247                                "{}{}{}".format(
2248                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2249                                    company,
2250                                    "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)),
2251                                ),
2252                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2253                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2254                            ))
2255
2256                    maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()])
2257                    info.extend([
2258                        "\n## Portfolio distribution by sectors\n"
2259                        "\n| Sector{} | Percent | Current cost       |\n".format(" " * (maxLenSectors - 6)),
2260                        "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)),
2261                    ])
2262
2263                    for sector in view["analytics"]["distrBySectors"].keys():
2264                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2265                            info.append("| {}{} | {:<7} | {:<18} |\n".format(
2266                                sector,
2267                                "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)),
2268                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2269                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2270                            ))
2271
2272                    maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()])
2273                    info.extend([
2274                        "\n## Portfolio distribution by currencies\n"
2275                        "\n| Instruments currencies{} | Percent | Current cost       |\n".format(" " * (maxLenMoney - 22)),
2276                        "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)),
2277                    ])
2278
2279                    for curr in view["analytics"]["distrByCurrencies"].keys():
2280                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2281                            nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"])
2282                            info.append("| {} | {:<7} | {:<18} |\n".format(
2283                                "[{}] {}{}".format(
2284                                    curr,
2285                                    view["analytics"]["distrByCurrencies"][curr]["name"],
2286                                    "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen),
2287                                ),
2288                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2289                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2290                            ))
2291
2292                    maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()]))
2293                    info.extend([
2294                        "\n## Portfolio distribution by countries\n"
2295                        "\n| Assets by country{} | Percent | Current cost       |\n".format(" " * (maxLenCountry - 17)),
2296                        "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)),
2297                    ])
2298
2299                    for country in view["analytics"]["distrByCountries"].keys():
2300                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2301                            nameLen = len(country)
2302                            info.append("| {} | {:<7} | {:<18} |\n".format(
2303                                "{}{}".format(
2304                                    country,
2305                                    "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen),
2306                                ),
2307                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2308                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2309                            ))
2310
2311            infoText = "".join(info)
2312
2313            uLogger.info(infoText)
2314
2315            if details == "full" and self.overviewFile:
2316                filename = self.overviewFile
2317
2318            elif details == "digest" and self.overviewDigestFile:
2319                filename = self.overviewDigestFile
2320
2321            elif details == "positions" and self.overviewPositionsFile:
2322                filename = self.overviewPositionsFile
2323
2324            elif details == "orders" and self.overviewOrdersFile:
2325                filename = self.overviewOrdersFile
2326
2327            elif details == "analytics" and self.overviewAnalyticsFile:
2328                filename = self.overviewAnalyticsFile
2329
2330            else:
2331                filename = ""
2332
2333            if filename:
2334                with open(filename, "w", encoding="UTF-8") as fH:
2335                    fH.write(infoText)
2336
2337                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2338
2339        return view

Get portfolio: all open positions, orders and some statistics for current accountId. If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile are defined then also save information to file.

WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.

Parameters
  • show: if False then only dictionary returns, if True then show more debug information.
  • details: how detailed should the information be? You should specify one of strings: full - shows full available information about portfolio status (by default), positions - shows only open positions, digest - show a short digest of the portfolio status, analytics - shows only the analytics section and the distribution of the portfolio by various categories, orders - shows only sections of open limits and stop orders.
Returns

dictionary with client's raw portfolio and some statistics.

def Deals( self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple:
2341    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple:
2342        """
2343        Returns history operations between two given dates for current `accountId`.
2344        If `reportFile` string is not empty then also save human-readable report.
2345        Shows some statistical data of closed positions.
2346
2347        :param start: see docstring in `GetDatesAsString()` method
2348        :param end: see docstring in `GetDatesAsString()` method
2349        :param show: if `True` then also prints all records to the console.
2350        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2351        :return: original list of dictionaries with history of deals records from API ("operations" key):
2352                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2353                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2354        """
2355        if self.accountId is None or not self.accountId:
2356            uLogger.error("Variable `accountId` must be defined for using this method!")
2357            raise Exception("Account ID required")
2358
2359        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2360
2361        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2362
2363        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2364        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2365        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2366        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2367        customStat = {}  # custom statistics in additional to responseJSON
2368
2369        # --- output report in human-readable format:
2370        if show or self.reportFile:
2371            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2372            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2373            nextDay = ""
2374
2375            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2376
2377            if len(ops) > 0:
2378                customStat = {
2379                    "opsCount": 0,  # total operations count
2380                    "buyCount": 0,  # buy operations
2381                    "sellCount": 0,  # sell operations
2382                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2383                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2384                    "payIn": {"rub": 0.},  # Deposit brokerage account
2385                    "payOut": {"rub": 0.},  # Withdrawals
2386                    "divs": {"rub": 0.},  # Dividends income
2387                    "coupons": {"rub": 0.},  # Coupon's income
2388                    "brokerCom": {"rub": 0.},  # Service commissions
2389                    "serviceCom": {"rub": 0.},  # Service commissions
2390                    "marginCom": {"rub": 0.},  # Margin commissions
2391                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2392                }
2393
2394                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2395                for item in ops:
2396                    if item["state"] == "OPERATION_STATE_EXECUTED":
2397                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2398
2399                        # count buy operations:
2400                        if "_BUY" in item["operationType"]:
2401                            customStat["buyCount"] += 1
2402
2403                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2404                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2405
2406                            else:
2407                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2408
2409                        # count sell operations:
2410                        elif "_SELL" in item["operationType"]:
2411                            customStat["sellCount"] += 1
2412
2413                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2414                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2415
2416                            else:
2417                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2418
2419                        # count incoming operations:
2420                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2421                            if item["payment"]["currency"] in customStat["payIn"].keys():
2422                                customStat["payIn"][item["payment"]["currency"]] += payment
2423
2424                            else:
2425                                customStat["payIn"][item["payment"]["currency"]] = payment
2426
2427                        # count withdrawals operations:
2428                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2429                            if item["payment"]["currency"] in customStat["payOut"].keys():
2430                                customStat["payOut"][item["payment"]["currency"]] += payment
2431
2432                            else:
2433                                customStat["payOut"][item["payment"]["currency"]] = payment
2434
2435                        # count dividends income:
2436                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2437                            if item["payment"]["currency"] in customStat["divs"].keys():
2438                                customStat["divs"][item["payment"]["currency"]] += payment
2439
2440                            else:
2441                                customStat["divs"][item["payment"]["currency"]] = payment
2442
2443                        # count coupon's income:
2444                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2445                            if item["payment"]["currency"] in customStat["coupons"].keys():
2446                                customStat["coupons"][item["payment"]["currency"]] += payment
2447
2448                            else:
2449                                customStat["coupons"][item["payment"]["currency"]] = payment
2450
2451                        # count broker commissions:
2452                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2453                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2454                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2455
2456                            else:
2457                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2458
2459                        # count service commissions:
2460                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2461                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2462                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2463
2464                            else:
2465                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2466
2467                        # count margin commissions:
2468                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2469                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2470                                customStat["marginCom"][item["payment"]["currency"]] += payment
2471
2472                            else:
2473                                customStat["marginCom"][item["payment"]["currency"]] = payment
2474
2475                        # count withholding taxes:
2476                        elif "_TAX" in item["operationType"]:
2477                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2478                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2479
2480                            else:
2481                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2482
2483                        else:
2484                            continue
2485
2486                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2487
2488                # --- view "Actions" lines:
2489                info.extend([
2490                    "| 1                          | 2                             | 3                            | 4                    | 5                      |\n",
2491                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2492                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2493                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2494                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2495                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2496                    ),
2497                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2498                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2499                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2500                    ),
2501                ])
2502
2503                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2504                for key in opsKeys:
2505                    if key == "rub":
2506                        continue
2507
2508                    info.extend([
2509                        "|                            |                               | {:<28} |                      |                        |\n".format(
2510                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2511                        ),
2512                        "|                            |                               | {:<28} |                      |                        |\n".format(
2513                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2514                        ),
2515                    ])
2516
2517                info.append(splitLine1)
2518
2519                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2520                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2521                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2522                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2523                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2524                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2525                    )
2526
2527                # --- view "Payments" lines:
2528                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2529                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2530
2531                for key in paymentsKeys:
2532                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2533
2534                info.append(splitLine1)
2535
2536                # --- view "Commissions and taxes" lines:
2537                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2538                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2539
2540                for key in comKeys:
2541                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2542
2543                info.append(splitLine1)
2544
2545                info.extend([
2546                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2547                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2548                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2549                ])
2550
2551            else:
2552                info.append("Broker returned no operations during this period\n")
2553
2554            # --- view "Operations" section:
2555            for item in ops:
2556                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2557                    continue
2558
2559                else:
2560                    self.figi = item["figi"] if item["figi"] else ""
2561                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2562                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2563
2564                    # group of deals during one day:
2565                    if nextDay and item["date"].split("T")[0] != nextDay:
2566                        info.append(splitLine2)
2567                        nextDay = ""
2568
2569                    else:
2570                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2571
2572                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2573                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2574                        self.figi if self.figi else "—",
2575                        instrument["ticker"] if instrument else "—",
2576                        instrument["type"] if instrument else "—",
2577                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2578                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2579                        TKS_OPERATION_STATES[item["state"]],
2580                        TKS_OPERATION_TYPES[item["operationType"]],
2581                    ))
2582
2583            infoText = "".join(info)
2584
2585            if show:
2586                uLogger.info(infoText)
2587
2588            if self.reportFile:
2589                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2590                    fH.write(infoText)
2591
2592                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2593
2594        return ops, customStat

Returns history operations between two given dates for current accountId. If reportFile string is not empty then also save human-readable report. Shows some statistical data of closed positions.

Parameters
  • start: see docstring in GetDatesAsString() method
  • end: see docstring in GetDatesAsString() method
  • show: if True then also prints all records to the console.
  • showCancelled: if False then remove information about cancelled operations from the deals report.
Returns

original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.

def History( self, start: str = None, end: str = None, interval: str = 'hour', onlyMissing: bool = False, csvSep: str = ',', show: bool = False) -> pandas.core.frame.DataFrame:
2596    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2597        """
2598        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2599
2600        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2601        Warning! Broker server used ISO UTC time by default.
2602
2603        If `historyFile` is not `None` then method save history to file, otherwise return only pandas dataframe.
2604        Also, `historyFile` used to update history with `onlyMissing` parameter.
2605
2606        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2607
2608        :param start: see docstring in `GetDatesAsString()` method.
2609        :param end: see docstring in `GetDatesAsString()` method.
2610        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2611                         `"hour"`, `"day"`. Default: `"hour"`.
2612        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2613                            False by default. Warning! History appends only from last candle to current time
2614                            with always update last candle!
2615        :param csvSep: separator if csv-file is used, `,` by default.
2616        :param show: if `True` then also prints pandas dataframe to the console.
2617        :return: pandas dataframe with prices history. Headers of columns are defined by default:
2618                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2619        """
2620        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2621        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2622        history = None  # empty pandas object for history
2623
2624        if interval not in TKS_CANDLE_INTERVALS.keys():
2625            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2626            raise Exception("Incorrect value")
2627
2628        if not (self.ticker or self.figi):
2629            uLogger.error("Ticker or FIGI must be defined!")
2630            raise Exception("Ticker or FIGI required")
2631
2632        if self.ticker and not self.figi:
2633            instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False)
2634            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2635
2636        if self.figi and not self.ticker:
2637            instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False)
2638            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2639
2640        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2641        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2642        if interval.lower() != "day":
2643            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2644
2645        delta = dtEnd - dtStart  # current UTC time minus last time in file
2646        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2647
2648        # calculate history length in candles:
2649        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2650        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2651            length += 1  # to avoid fraction time
2652
2653        # calculate data blocks count:
2654        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2655
2656        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2657        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2658        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2659        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2660        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2661
2662        tempOld = None  # pandas object for old history, if --only-missing key present
2663        lastTime = None  # datetime object of last old candle in file
2664
2665        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2666            uLogger.debug("--only-missing key present, add only last missing candles...")
2667            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2668
2669            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2670
2671            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2672            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2673            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2674            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2675
2676            # get last datetime object from last string in file or minus 1 delta if file is empty:
2677            if len(tempOld) > 0:
2678                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2679
2680            else:
2681                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2682
2683            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2684
2685        responseJSONs = []  # raw history blocks of data
2686
2687        blockEnd = dtEnd
2688        for item in range(blocks):
2689            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2690            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2691
2692            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2693                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2694            ))
2695
2696            if blockStart == blockEnd:
2697                uLogger.debug("Skipped this zero-length block...")
2698
2699            else:
2700                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2701                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2702                self.body = str({
2703                    "figi": self.figi,
2704                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2705                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2706                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2707                })
2708                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False)
2709
2710                if "code" in responseJSON.keys():
2711                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2712
2713                else:
2714                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2715                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2716
2717                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2718
2719            blockEnd = blockStart
2720
2721        printCount = len(responseJSONs)  # candles to show in console
2722        if responseJSONs:
2723            tempHistory = pd.DataFrame(
2724                data={
2725                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2726                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2727                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2728                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2729                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2730                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2731                    "volume": [int(item["volume"]) for item in responseJSONs],
2732                },
2733                index=range(len(responseJSONs)),
2734                columns=["date", "time", "open", "high", "low", "close", "volume"],
2735            )
2736            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2737            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2738
2739            # append only newest candles to old history if --only-missing key present:
2740            if onlyMissing and tempOld is not None and lastTime is not None:
2741                index = 0  # find start index in tempHistory data:
2742
2743                for i, item in tempHistory.iterrows():
2744                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2745
2746                    if curTime == lastTime:
2747                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2748                        index = i
2749                        printCount = index + 1
2750                        break
2751
2752                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2753
2754            else:
2755                history = tempHistory  # if no `--only-missing` key then load full data from server
2756
2757            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2758
2759        if history is not None and not history.empty:
2760            if show:
2761                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2762                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2763                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2764                ))
2765
2766        else:
2767            uLogger.warning("Received an empty candles history!")
2768
2769        if self.historyFile is not None:
2770            if history is not None and not history.empty:
2771                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2772                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2773
2774            else:
2775                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2776
2777        else:
2778            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only pandas dataframe returns.")
2779
2780        return history

This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).

History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01. Warning! Broker server used ISO UTC time by default.

If historyFile is not None then method save history to file, otherwise return only pandas dataframe. Also, historyFile used to update history with onlyMissing parameter.

See also: LoadHistory() and ShowHistoryChart() methods.

Parameters
  • start: see docstring in GetDatesAsString() method.
  • end: see docstring in GetDatesAsString() method.
  • interval: this is a candle interval. Current available values are "1min", "5min", "15min", "hour", "day". Default: "hour".
  • onlyMissing: if True then add only last missing candles, do not request all history length from start. False by default. Warning! History appends only from last candle to current time with always update last candle!
  • csvSep: separator if csv-file is used, , by default.
  • show: if True then also prints pandas dataframe to the console.
Returns

pandas dataframe with prices history. Headers of columns are defined by default: ["date", "time", "open", "high", "low", "close", "volume"].

def LoadHistory(self, filePath: str) -> pandas.core.frame.DataFrame:
2782    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2783        """
2784        Load candles history from csv-file and return pandas dataframe object.
2785
2786        See also: `History()` and `ShowHistoryChart()` methods.
2787
2788        :param filePath: path to csv-file to open.
2789        """
2790        loadedHistory = None  # init candles data object
2791
2792        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2793
2794        if os.path.exists(filePath):
2795            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as pandas dataframe
2796
2797            tfStr = self.priceModel.FormattedDelta(
2798                self.priceModel.timeframe,
2799                "{days} days {hours}h {minutes}m {seconds}s",
2800            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2801                self.priceModel.timeframe,
2802                "{hours}h {minutes}m {seconds}s",
2803            )
2804
2805            if loadedHistory is not None and not loadedHistory.empty:
2806                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2807                    len(loadedHistory),
2808                    tfStr,
2809                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2810                )
2811
2812            else:
2813                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2814
2815        else:
2816            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2817
2818        return loadedHistory

Load candles history from csv-file and return pandas dataframe object.

See also: History() and ShowHistoryChart() methods.

Parameters
  • filePath: path to csv-file to open.
def ShowHistoryChart( self, candles: Union[str, pandas.core.frame.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2820    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2821        """
2822        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2823
2824        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2825        Default: `index.html` (both for interact and non-interact candlesticks chart).
2826
2827        See also: `History()` and `LoadHistory()` methods.
2828
2829        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2830        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2831                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2832                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2833                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2834        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2835                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2836        """
2837        if isinstance(candles, str):
2838            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2839            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2840
2841        elif isinstance(candles, pd.DataFrame):
2842            self.priceModel.prices = candles  # set candles chain from variable
2843            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2844
2845            if "datetime" not in candles.columns:
2846                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2847
2848        else:
2849            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2850            raise Exception("Incorrect value")
2851
2852        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2853
2854        if interact:
2855            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2856
2857            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2858
2859        else:
2860            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2861
2862            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2863
2864        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))

Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.

Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart. Default: index.html (both for interact and non-interact candlesticks chart).

See also: History() and LoadHistory() methods.

Parameters
def Trade( self, operation: str, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2866    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2867        """
2868        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2869        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2870
2871        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2872
2873        :param operation: string "Buy" or "Sell".
2874        :param lots: volume, integer count of lots >= 1.
2875        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2876        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2877        :param expDate: string "Undefined" by default or local date in future,
2878                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2879        :return: JSON with response from broker server.
2880        """
2881        if self.accountId is None or not self.accountId:
2882            uLogger.error("Variable `accountId` must be defined for using this method!")
2883            raise Exception("Account ID required")
2884
2885        if operation is None or not operation or operation not in ("Buy", "Sell"):
2886            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2887            raise Exception("Incorrect value")
2888
2889        if lots is None or lots < 1:
2890            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2891            lots = 1
2892
2893        if tp is None or tp < 0:
2894            tp = 0
2895
2896        if sl is None or sl < 0:
2897            sl = 0
2898
2899        if expDate is None or not expDate:
2900            expDate = "Undefined"
2901
2902        if not (self.ticker or self.figi):
2903            uLogger.error("Ticker or FIGI must be defined!")
2904            raise Exception("Ticker or FIGI required")
2905
2906        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
2907        self.ticker = instrument["ticker"]
2908        self.figi = instrument["figi"]
2909
2910        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2911
2912        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2913        self.body = str({
2914            "figi": self.figi,
2915            "quantity": str(lots),
2916            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2917            "accountId": str(self.accountId),
2918            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2919        })
2920        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False)
2921
2922        if "orderId" in response.keys():
2923            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2924                operation, response["orderId"],
2925                self.ticker, self.figi, lots,
2926                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2927                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2928                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2929            ))
2930
2931        else:
2932            uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.")
2933
2934        if tp > 0:
2935            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2936
2937        if sl > 0:
2938            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2939
2940        return response

Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().

Parameters
  • operation: string "Buy" or "Sell".
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter targetPrice in self.Order().
  • sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter targetPrice in self.Order().
  • expDate: string "Undefined" by default or local date in future, it is a string with format %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Buy( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2942    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2943        """
2944        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2945        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2946
2947        See also: `Order()` and `Trade()` docstrings.
2948
2949        :param lots: volume, integer count of lots >= 1.
2950        :param tp: float > 0, take profit price of stop-order.
2951        :param sl: float > 0, stop loss price of stop-order.
2952        :param expDate: it's a local date in future.
2953                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2954        :return: JSON with response from broker server.
2955        """
2956        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Sell( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2958    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2959        """
2960        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2961        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2962
2963        See also: `Order()` and `Trade()` docstrings.
2964
2965        :param lots: volume, integer count of lots >= 1.
2966        :param tp: float > 0, take profit price of stop-order.
2967        :param sl: float > 0, stop loss price of stop-order.
2968        :param expDate: it's a local date in the future.
2969                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2970        :return: JSON with response from broker server.
2971        """
2972        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in the future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def CloseTrades(self, tickers: list, portfolio: dict = None) -> None:
2974    def CloseTrades(self, tickers: list, portfolio: dict = None) -> None:
2975        """
2976        Close position of given instruments.
2977
2978        :param tickers: tickers list of instruments that must be closed.
2979        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
2980                         This avoids unnecessary downloading data from the server.
2981        """
2982        if not tickers:
2983            uLogger.info("Tickers list is empty, nothing to close.")
2984
2985        else:
2986            if portfolio is None or not portfolio:
2987                portfolio = self.Overview(show=False)
2988
2989            allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
2990            uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers))
2991
2992            for ticker in tickers:
2993                if ticker not in allOpenedTickers:
2994                    uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker))
2995                    continue
2996
2997                # search open trade info about instrument by ticker:
2998                instrument = {}
2999                for iType in TKS_INSTRUMENTS:
3000                    if instrument:
3001                        break
3002
3003                    for item in portfolio["stat"][iType]:
3004                        if item["ticker"] == ticker:
3005                            instrument = item
3006                            break
3007
3008                if instrument:
3009                    self.ticker = ticker
3010                    self.figi = instrument["figi"]
3011
3012                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3013                        self.ticker,
3014                        self.figi,
3015                        int(instrument["volume"]),
3016                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3017                    ))
3018
3019                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3020
3021                    if tradeLots > 0:
3022                        if instrument["blocked"] > 0:
3023                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3024                                instrument["blocked"],
3025                                self.ticker,
3026                                tradeLots,
3027                            ))
3028
3029                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3030                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3031
3032                    else:
3033                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))

Close position of given instruments.

Parameters
  • tickers: tickers list of instruments that must be closed.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3035    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3036        """
3037        Close all positions of given instruments with defined type.
3038
3039        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3040        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3041                         This avoids unnecessary downloading data from the server.
3042        """
3043        if iType not in TKS_INSTRUMENTS:
3044            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3045
3046        else:
3047            if portfolio is None or not portfolio:
3048                portfolio = self.Overview(show=False)
3049
3050            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3051            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3052
3053            if tickers and portfolio:
3054                self.CloseTrades(tickers, portfolio)
3055
3056            else:
3057                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))

Close all positions of given instruments with defined type.

Parameters
  • iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def Order( self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3059    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3060        """
3061        Universal method to create market or limit orders with all available parameters for current `accountId`.
3062        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3063
3064        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3065        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3066
3067        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3068        then broker immediately open market order as you can do simple --buy or --sell operations!
3069
3070        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3071        When current price will go up or down to target price value then broker opens a limit order.
3072        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3073
3074        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3075
3076        :param operation: string "Buy" or "Sell".
3077        :param orderType: string "Limit" or "Stop".
3078        :param lots: volume, integer count of lots >= 1.
3079        :param targetPrice: target price > 0. This is open trade price for limit order.
3080        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3081                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3082        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3083                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3084                         Stop loss order always executed by market price.
3085        :param expDate: string "Undefined" by default or local date in future.
3086                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3087                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3088                        A limit order has no expiration date, it lasts until the end of the trading day.
3089        :return: JSON with response from broker server.
3090        """
3091        if self.accountId is None or not self.accountId:
3092            uLogger.error("Variable `accountId` must be defined for using this method!")
3093            raise Exception("Account ID required")
3094
3095        if operation is None or not operation or operation not in ("Buy", "Sell"):
3096            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3097            raise Exception("Incorrect value")
3098
3099        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3100            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3101            raise Exception("Incorrect value")
3102
3103        if lots is None or lots < 1:
3104            uLogger.error("You must define trade volume > 0: integer count of lots!")
3105            raise Exception("Incorrect value")
3106
3107        if targetPrice is None or targetPrice <= 0:
3108            uLogger.error("Target price for limit-order must be greater than 0!")
3109            raise Exception("Incorrect value")
3110
3111        if limitPrice is None or limitPrice <= 0:
3112            limitPrice = targetPrice
3113
3114        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3115            stopType = "Limit"
3116
3117        if expDate is None or not expDate:
3118            expDate = "Undefined"
3119
3120        if not (self.ticker or self.figi):
3121            uLogger.error("Tocker or FIGI must be defined!")
3122            raise Exception("Ticker or FIGI required")
3123
3124        response = {}
3125        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
3126        self.ticker = instrument["ticker"]
3127        self.figi = instrument["figi"]
3128
3129        if orderType == "Limit":
3130            uLogger.debug(
3131                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3132                    self.ticker, self.figi,
3133                    operation, lots, targetPrice, instrument["currency"],
3134                ))
3135
3136            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3137            self.body = str({
3138                "figi": self.figi,
3139                "quantity": str(lots),
3140                "price": FloatToNano(targetPrice),
3141                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3142                "accountId": str(self.accountId),
3143                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3144            })
3145            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3146
3147            if "orderId" in response.keys():
3148                uLogger.info(
3149                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3150                        response["orderId"],
3151                        self.ticker, self.figi,
3152                        operation, lots, targetPrice, instrument["currency"],
3153                    ))
3154
3155                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3156                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3157                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3158                            targetPrice, instrument["currency"],
3159                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3160                        ))
3161
3162                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3163                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3164                            targetPrice, instrument["currency"],
3165                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3166                        ))
3167
3168            else:
3169                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3170
3171        if orderType == "Stop":
3172            uLogger.debug(
3173                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3174                    self.ticker, self.figi,
3175                    operation, lots,
3176                    targetPrice, instrument["currency"],
3177                    limitPrice, instrument["currency"],
3178                    stopType, expDate,
3179                ))
3180
3181            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3182            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3183            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3184
3185            body = {
3186                "figi": self.figi,
3187                "quantity": str(lots),
3188                "price": FloatToNano(limitPrice),
3189                "stopPrice": FloatToNano(targetPrice),
3190                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3191                "accountId": str(self.accountId),
3192                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3193                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3194            }
3195
3196            if expDateUTC:
3197                body["expireDate"] = expDateUTC
3198
3199            self.body = str(body)
3200            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3201
3202            if "stopOrderId" in response.keys():
3203                uLogger.info(
3204                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3205                        response["stopOrderId"],
3206                        self.ticker, self.figi,
3207                        operation, lots,
3208                        targetPrice, instrument["currency"],
3209                        limitPrice, instrument["currency"],
3210                        TKS_STOP_ORDER_TYPES[stopOrderType],
3211                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3212                    ))
3213
3214                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3215                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3216                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3217                            targetPrice, instrument["currency"],
3218                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3219                        ))
3220
3221                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3222                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3223                            targetPrice, instrument["currency"],
3224                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3225                        ))
3226
3227            else:
3228                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3229
3230        return response

Universal method to create market or limit orders with all available parameters for current accountId. See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().

If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.

Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!

If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.

Only one attempt and no retry for opens order. If network issue occurred you can create new request.

Parameters
  • operation: string "Buy" or "Sell".
  • orderType: string "Limit" or "Stop".
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
  • limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
  • stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns

JSON with response from broker server.

def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3232    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3233        """
3234        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3235        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3236        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3237        See also: `Order()` docstring.
3238
3239        :param lots: volume, integer count of lots >= 1.
3240        :param targetPrice: target price > 0. This is open trade price for limit order.
3241        :return: JSON with response from broker server.
3242        """
3243        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Buy limit-order (below current price). You must specify only 2 parameters: lots and target price to open buy limit-order. If you try to create buy limit-order above current price then broker immediately open Buy market order, such as if you do simple --buy operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def BuyStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3245    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3246        """
3247        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3248        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3249        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3250        target price value then broker opens a limit order. See also: `Order()` docstring.
3251
3252        :param lots: volume, integer count of lots >= 1.
3253        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3254        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3255                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3256        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3257                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3258        :param expDate: string "Undefined" by default or local date in future.
3259                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3260                        This date is converting to UTC format for server.
3261        :return: JSON with response from broker server.
3262        """
3263        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order. In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for buy stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def SellLimit(self, lots: int, targetPrice: float) -> dict:
3265    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3266        """
3267        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3268        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3269        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3270        See also: `Order()` docstring.
3271
3272        :param lots: volume, integer count of lots >= 1.
3273        :param targetPrice: target price > 0. This is open trade price for limit order.
3274        :return: JSON with response from broker server.
3275        """
3276        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Sell limit-order (above current price). You must specify only 2 parameters: lots and target price to open sell limit-order. If you try to create sell limit-order below current price then broker immediately open Sell market order, such as if you do simple --sell operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def SellStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3278    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3279        """
3280        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3281        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3282        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3283        target price value then broker opens a limit order. See also: `Order()` docstring.
3284
3285        :param lots: volume, integer count of lots >= 1.
3286        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3287        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3288                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3289        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3290                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3291        :param expDate: string "Undefined" by default or local date in future.
3292                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3293                        This date is converting to UTC format for server.
3294        :return: JSON with response from broker server.
3295        """
3296        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order. In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for sell stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def CloseOrders( self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3298    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3299        """
3300        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3301
3302        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3303        :param allOrdersIDs: pre-received lists of all active pending orders.
3304                             This avoids unnecessary downloading data from the server.
3305        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3306        """
3307        if self.accountId is None or not self.accountId:
3308            uLogger.error("Variable `accountId` must be defined for using this method!")
3309            raise Exception("Account ID required")
3310
3311        if orderIDs:
3312            if allOrdersIDs is None or not allOrdersIDs:
3313                rawOrders = self.RequestPendingOrders()
3314                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3315
3316            if allStopOrdersIDs is None or not allStopOrdersIDs:
3317                rawStopOrders = self.RequestStopOrders()
3318                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3319
3320            for orderID in orderIDs:
3321                idInPendingOrders = orderID in allOrdersIDs
3322                idInStopOrders = orderID in allStopOrdersIDs
3323
3324                if not (idInPendingOrders or idInStopOrders):
3325                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3326                    continue
3327
3328                else:
3329                    if idInPendingOrders:
3330                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3331
3332                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3333                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3334                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3335                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3336
3337                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3338                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3339                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3340
3341                        else:
3342                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3343
3344                    elif idInStopOrders:
3345                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3346
3347                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3348                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3349                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3350                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3351
3352                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3353                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3354                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3355
3356                        else:
3357                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3358
3359                    else:
3360                        continue

Cancel order or list of orders by its orderId or stopOrderId for current accountId.

Parameters
  • orderIDs: list of integers with orderId or stopOrderId.
  • allOrdersIDs: pre-received lists of all active pending orders. This avoids unnecessary downloading data from the server.
  • allStopOrdersIDs: pre-received lists of all active stop orders.
def CloseAllOrders(self) -> None:
3362    def CloseAllOrders(self) -> None:
3363        """
3364        Gets a list of open pending and stop orders and cancel it all.
3365        """
3366        rawOrders = self.RequestPendingOrders()
3367        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3368        lenOrders = len(allOrdersIDs)
3369
3370        rawStopOrders = self.RequestStopOrders()
3371        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3372        lenSOrders = len(allStopOrdersIDs)
3373
3374        if lenOrders > 0 or lenSOrders > 0:
3375            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3376
3377            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3378
3379        else:
3380            uLogger.info("Orders not found, nothing to cancel.")

Gets a list of open pending and stop orders and cancel it all.

def CloseAll(self, *args) -> None:
3382    def CloseAll(self, *args) -> None:
3383        """
3384        Close all available (not blocked) opened trades and orders.
3385
3386        Also, you can select one or more keywords case-insensitive:
3387        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3388
3389        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3390        """
3391        overview = self.Overview(show=False)  # get all open trades info
3392
3393        if len(args) == 0:
3394            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3395            self.CloseAllOrders()  # close all pending and stop orders
3396
3397            for iType in TKS_INSTRUMENTS:
3398                if iType != "Currencies":
3399                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3400
3401        else:
3402            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3403            lowerArgs = [x.lower() for x in args]
3404
3405            if "orders" in lowerArgs:
3406                self.CloseAllOrders()  # close all pending and stop orders
3407
3408            for iType in TKS_INSTRUMENTS:
3409                if iType.lower() in lowerArgs and iType != "Currencies":
3410                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies

Close all available (not blocked) opened trades and orders.

Also, you can select one or more keywords case-insensitive: orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.

Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.

@staticmethod
def ParseOrderParameters(operation, **inputParameters)
3412    @staticmethod
3413    def ParseOrderParameters(operation, **inputParameters):
3414        """
3415        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3416
3417        :param operation: string "Buy" or "Sell".
3418        :param inputParameters: this is dict of strings that looks like this
3419               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3420               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3421               "prices" key: one or more prices to open limit-orders
3422               Counts of values in lots and prices lists must be equals!
3423        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3424        """
3425        # TODO: update order grid work with api v2
3426        pass
3427        # uLogger.debug("Input parameters: {}".format(inputParameters))
3428        #
3429        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3430        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3431        #     raise Exception("Incorrect value")
3432        #
3433        # if "l" in inputParameters.keys():
3434        #     inputParameters["lots"] = inputParameters.pop("l")
3435        #
3436        # if "p" in inputParameters.keys():
3437        #     inputParameters["prices"] = inputParameters.pop("p")
3438        #
3439        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3440        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3441        #     raise Exception("Incorrect value")
3442        #
3443        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3444        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3445        #
3446        # if len(lots) != len(prices):
3447        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3448        #     raise Exception("Incorrect value")
3449        #
3450        # uLogger.debug("Extracted parameters for orders:")
3451        # uLogger.debug("lots = {}".format(lots))
3452        # uLogger.debug("prices = {}".format(prices))
3453        #
3454        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3455        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3456        # uLogger.debug("Order parameters: {}".format(result))
3457        #
3458        # return result

Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.

Parameters
  • operation: string "Buy" or "Sell".
  • inputParameters: this is dict of strings that looks like this {"lots": "L_int,...", "prices": "P_float,..."} where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns

list of dictionaries with all lots and prices to open orders that looks like this [{"lot": lots_1, "price": price_1}, {...}, ...]

def IsInPortfolio(self, portfolio: dict = None) -> bool:
3460    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3461        """
3462        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3463
3464        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3465        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3466        """
3467        result = False
3468        msg = "Instrument not defined!"
3469
3470        if portfolio is None or not portfolio:
3471            portfolio = self.Overview(show=False)
3472
3473        if self.ticker:
3474            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3475            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3476
3477            for iType in TKS_INSTRUMENTS:
3478                for instrument in portfolio["stat"][iType]:
3479                    if instrument["ticker"] == self.ticker:
3480                        result = True
3481                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3482                        break
3483
3484        elif self.figi:
3485            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3486            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3487
3488            for iType in TKS_INSTRUMENTS:
3489                for instrument in portfolio["stat"][iType]:
3490                    if instrument["figi"] == self.figi:
3491                        result = True
3492                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3493                        break
3494
3495        else:
3496            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3497
3498        uLogger.debug(msg)
3499
3500        return result

Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if portfolio contains open position with given instrument, False otherwise.

def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3502    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3503        """
3504        Returns instrument is in the user's portfolio if it presents there.
3505        Instrument must be defined by `ticker` (highly priority) or `figi`.
3506
3507        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3508        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3509        """
3510        result = None
3511        msg = "Instrument not defined!"
3512
3513        if portfolio is None or not portfolio:
3514            portfolio = self.Overview(show=False)
3515
3516        if self.ticker:
3517            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3518            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3519
3520            for iType in TKS_INSTRUMENTS:
3521                for instrument in portfolio["stat"][iType]:
3522                    if instrument["ticker"] == self.ticker:
3523                        result = instrument
3524                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3525                        break
3526
3527        elif self.figi:
3528            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3529            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3530
3531            for iType in TKS_INSTRUMENTS:
3532                for instrument in portfolio["stat"][iType]:
3533                    if instrument["figi"] == self.figi:
3534                        result = instrument
3535                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3536                        break
3537
3538        else:
3539            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3540
3541        uLogger.debug(msg)
3542
3543        return result

Returns instrument is in the user's portfolio if it presents there. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

dict with instrument if portfolio contains open position with this instrument, None otherwise.

def RequestLimits(self) -> dict:
3545    def RequestLimits(self) -> dict:
3546        """
3547        Method for obtaining the available funds for withdrawal for current `accountId`.
3548
3549        See also:
3550        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3551        - `OverviewLimits()` method
3552
3553        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3554                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3555                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3556                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3557        """
3558        if self.accountId is None or not self.accountId:
3559            uLogger.error("Variable `accountId` must be defined for using this method!")
3560            raise Exception("Account ID required")
3561
3562        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3563
3564        self.body = str({"accountId": self.accountId})
3565        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3566        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3567
3568        uLogger.debug("Records about available funds for withdrawal successfully received")
3569
3570        return rawLimits

Method for obtaining the available funds for withdrawal for current accountId.

See also:

Returns

dict with raw data from server that contains free funds for withdrawal. Example of dict: {"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Here money is an array of portfolio currency positions, blocked is an array of blocked currency positions of the portfolio and blockedGuarantee is locked money under collateral for futures.

def OverviewLimits(self, show: bool = False) -> dict:
3572    def OverviewLimits(self, show: bool = False) -> dict:
3573        """
3574        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3575
3576        See also: `RequestLimits()`.
3577
3578        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3579        :return: dict with raw parsed data from server and some calculated statistics about it.
3580        """
3581        if self.accountId is None or not self.accountId:
3582            uLogger.error("Variable `accountId` must be defined for using this method!")
3583            raise Exception("Account ID required")
3584
3585        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3586
3587        view = {
3588            "rawLimits": rawLimits,
3589            "limits": {  # parsed data for every currency:
3590                "money": {  # this is an array of portfolio currency positions
3591                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3592                },
3593                "blocked": {  # this is an array of blocked currency
3594                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3595                },
3596                "blockedGuarantee": {  # this is locked money under collateral for futures
3597                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3598                },
3599            },
3600        }
3601
3602        # --- Prepare text table with limits in human-readable format:
3603        if show:
3604            info = [
3605                "# Withdrawal limits\n\n",
3606                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3607                "* **Account ID:** [{}]\n".format(self.accountId),
3608                "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3609                "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3610            ]
3611
3612            for curr in view["limits"]["money"].keys():
3613                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3614                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3615                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3616
3617                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3618                    "[{}]".format(curr),
3619                    "{:.2f}".format(view["limits"]["money"][curr]),
3620                    "{:.2f}".format(availableMoney),
3621                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3622                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3623                )
3624
3625                if curr == "rub":
3626                    info.insert(5, infoStr)  # insert at first position in table and after headers
3627
3628                else:
3629                    info.append(infoStr)
3630
3631            infoText = "".join(info)
3632
3633            uLogger.info(infoText)
3634
3635            if self.withdrawalLimitsFile:
3636                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3637                    fH.write(infoText)
3638
3639                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3640
3641        return view

Method for parsing and show table with available funds for withdrawal for current accountId.

See also: RequestLimits().

Parameters
  • show: if False then only dictionary returns, if True then also print withdrawal limits to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

def RequestAccounts(self) -> dict:
3643    def RequestAccounts(self) -> dict:
3644        """
3645        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3646
3647        See also:
3648        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3649        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3650        - `OverviewUserInfo()` method
3651
3652        :return: dict with raw data from server that contains accounts info. Example of dict:
3653                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3654                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3655                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3656                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3657        """
3658        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3659
3660        self.body = str({})
3661        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3662        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3663
3664        uLogger.debug("Records about available accounts successfully received")
3665
3666        return rawAccounts

Method for requesting all brokerage accounts (accountIds) of current user detected by token.

See also:

Returns

dict with raw data from server that contains accounts info. Example of dict: {"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. If closedDate="1970-01-01T00:00:00Z" it means that account is active now.

def RequestUserInfo(self) -> dict:
3668    def RequestUserInfo(self) -> dict:
3669        """
3670        Method for requesting common user's information.
3671
3672        See also:
3673        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3674        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3675        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3676        - `OverviewUserInfo()` method
3677
3678        :return: dict with raw data from server that contains user's information. Example of dict:
3679                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3680                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3681        """
3682        uLogger.debug("Requesting common user's information. Wait, please...")
3683
3684        self.body = str({})
3685        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3686        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3687
3688        uLogger.debug("Records about current user successfully received")
3689
3690        return rawUserInfo

Method for requesting common user's information.

See also:

Returns

dict with raw data from server that contains user's information. Example of dict: {"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.

def RequestMarginStatus(self, accountId: str = None) -> dict:
3692    def RequestMarginStatus(self, accountId: str = None) -> dict:
3693        """
3694        Method for requesting margin calculation for defined account ID.
3695
3696        See also:
3697        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3698        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3699        - `OverviewUserInfo()` method
3700
3701        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3702        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3703                 Example of responses:
3704                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3705                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3706                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3707                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3708                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3709                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3710        """
3711        if accountId is None or not accountId:
3712            if self.accountId is None or not self.accountId:
3713                uLogger.error("Variable `accountId` must be defined for using this method!")
3714                raise Exception("Account ID required")
3715
3716            else:
3717                accountId = self.accountId  # use `self.accountId` (main ID) by default
3718
3719        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3720
3721        self.body = str({"accountId": accountId})
3722        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3723        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3724
3725        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3726            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3727            rawMargin = {}
3728
3729        else:
3730            uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3731
3732        return rawMargin

Method for requesting margin calculation for defined account ID.

See also:

Parameters
  • accountId: string with numeric account ID. If None, then used class field accountId.
Returns

dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400: {"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns: {}. status code 200: {"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.

def RequestTariffLimits(self) -> dict:
3734    def RequestTariffLimits(self) -> dict:
3735        """
3736        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3737
3738        See also:
3739        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3740        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3741        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3742        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3743        - `OverviewUserInfo()` method
3744
3745        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3746                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3747                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3748        """
3749        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3750
3751        self.body = str({})
3752        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3753        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3754
3755        uLogger.debug("Records with limits of current tariff successfully received")
3756
3757        return rawTariffLimits

Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.

See also:

Returns

dict with raw data from server that contains limits of current tariff. Example of dict: {"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.

def RequestBondCoupons(self, iJSON: dict) -> dict:
3759    def RequestBondCoupons(self, iJSON: dict) -> dict:
3760        """
3761        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3762        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3763        All dates are in UTC timezone.
3764
3765        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3766        Documentation:
3767        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3768        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3769
3770        See also: `ExtendBondsData()`.
3771
3772        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3773                      If raw iJSON is not data of bond then server returns an error [400] with message:
3774                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3775        :return: dictionary with bond payment calendar. Response example
3776                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3777                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3778                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3779                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3780        """
3781        if iJSON["figi"] is None or not iJSON["figi"]:
3782            uLogger.error("FIGI must be defined for using this method!")
3783            raise Exception("FIGI required")
3784
3785        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3786        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3787
3788        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3789            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3790            self.figi,
3791            startDate,
3792            endDate,
3793        ))
3794
3795        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3796        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3797        calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False)
3798
3799        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3800            uLogger.warning("Instrument type is not bond!")
3801
3802        else:
3803            uLogger.debug("Records about bond payment calendar successfully received")
3804
3805        return calendar

Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". All dates are in UTC timezone.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:

See also: ExtendBondsData().

Parameters
  • iJSON: raw json data of a bond from broker server, example iJSON = self.iList["Bonds"][self.ticker] If raw iJSON is not data of bond then server returns an error [400] with message: {"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns

dictionary with bond payment calendar. Response example {"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}

def ExtendBondsData( self, instruments: list[str], xlsx: bool = False) -> pandas.core.frame.DataFrame:
3807    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3808        """
3809        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3810        pandas dataframe with more information about bonds: main info, current prices, bond payment calendar,
3811        coupon yields, current yields and some statistics etc.
3812
3813        WARNING! This is too long operation if a lot of bonds requested from broker server.
3814
3815        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3816
3817        :param instruments: list of strings with tickers or FIGIs.
3818        :param xlsx: if True then also exports pandas dataframe to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3819                     for further used by data scientists or stock analytics.
3820        :return: wider pandas dataframe with more full and calculated data about bonds, than raw response from broker.
3821                 In XLSX-file and pandas dataframe fields mean:
3822                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3823                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3824        """
3825        if instruments is None or not instruments:
3826            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3827            raise Exception("Ticker or FIGI required")
3828
3829        if isinstance(instruments, str):
3830            instruments = [instruments]
3831
3832        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3833
3834        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3835
3836        iCount = len(uniqueInstruments)
3837        tooLong = iCount >= 20
3838        if tooLong:
3839            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3840
3841        bonds = None
3842        for i, self.figi in enumerate(uniqueInstruments):
3843            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3844
3845            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3846                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3847                rawBond = self.SearchByFIGI(requestPrice=True)
3848
3849                # Widen raw data with UTC current time (iData["actualDateTime"]):
3850                actualDate = datetime.now(tzutc())
3851                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3852
3853                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3854                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3855
3856                # Replace some values with human-readable:
3857                iData["nominalCurrency"] = iData["nominal"]["currency"]
3858                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3859                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3860                iData["aciCurrency"] = iData["aciValue"]["currency"]
3861                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3862                iData["issueSize"] = int(iData["issueSize"])
3863                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3864                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3865                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3866                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3867                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3868                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3869                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3870                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3871                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3872                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3873
3874                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3875                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3876                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3877                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3878                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3879                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3880                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3881                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3882                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3883                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3884                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3885
3886                # Widen raw data with calendar data from `rawCalendar` values:
3887                calendarData = []
3888                for item in iData["rawCalendar"]["events"]:
3889                    calendarData.append({
3890                        "couponDate": item["couponDate"],
3891                        "couponNumber": int(item["couponNumber"]),
3892                        "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3893                        "payCurrency": item["payOneBond"]["currency"],
3894                        "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3895                        "couponType": TKS_COUPON_TYPES[item["couponType"]],
3896                        "couponStartDate": item["couponStartDate"],
3897                        "couponEndDate": item["couponEndDate"],
3898                        "couponPeriod": item["couponPeriod"],
3899                    })
3900
3901                # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3902                if "maturityDate" not in iData.keys():
3903                    iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3904
3905                # Widen raw data with Coupon Rate.
3906                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3907                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3908                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3909                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3910
3911                # Widen raw data with Yield to Maturity (YTM) on current date.
3912                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3913                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3914                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3915                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3916                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3917                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3918
3919                iData["calendar"] = calendarData  # adds calendar at the end
3920
3921                # Remove not used data:
3922                iData.pop("uid")
3923                iData.pop("positionUid")
3924                iData.pop("currentPrice")
3925                iData.pop("rawCalendar")
3926
3927                colNames = list(iData.keys())
3928                if bonds is None:
3929                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3930
3931                else:
3932                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
3933
3934            else:
3935                uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"]))
3936
3937            processed = round(100 * (i + 1) / iCount, 1)
3938            if tooLong and processed % 5 == 0:
3939                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
3940
3941            else:
3942                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
3943
3944        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
3945
3946        # Saving bonds from pandas dataframe to XLSX sheet:
3947        if xlsx and self.bondsXLSXFile:
3948            with pd.ExcelWriter(
3949                    path=self.bondsXLSXFile,
3950                    date_format=TKS_DATE_FORMAT,
3951                    datetime_format=TKS_DATE_TIME_FORMAT,
3952                    mode="w",
3953            ) as writer:
3954                bonds.to_excel(
3955                    writer,
3956                    sheet_name="Extended bonds data",
3957                    index=True,
3958                    encoding="UTF-8",
3959                    freeze_panes=(1, 1),
3960                )  # saving as XLSX-file with freeze first row and column as headers
3961
3962            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
3963
3964        return bonds

Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider pandas dataframe with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • xlsx: if True then also exports pandas dataframe to xlsx-file bondsXLSXFile, default ext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns

wider pandas dataframe with more full and calculated data about bonds, than raw response from broker. In XLSX-file and pandas dataframe fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon

def CreateBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, xlsx: bool = False) -> pandas.core.frame.DataFrame:
3966    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
3967        """
3968        Creates bond payments calendar as pandas dataframe, and also save it to the XLSX-file, `calendar.xlsx` by default.
3969
3970        WARNING! This is too long operation if a lot of bonds requested from broker server.
3971
3972        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
3973
3974        :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains
3975                        extended information about bonds: main info, current prices, bond payment calendar,
3976                        coupon yields, current yields and some statistics etc.
3977                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
3978        :param xlsx: if True then also exports pandas dataframe to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
3979                     for further used by data scientists or stock analytics.
3980        :return: pandas dataframe with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
3981        """
3982        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
3983            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
3984
3985        uLogger.debug("Generating bond payments calendar data. Wait, please...")
3986
3987        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
3988        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
3989        calendar = None
3990        for bond in extBonds.iterrows():
3991            for item in bond[1]["calendar"]:
3992                cData = {
3993                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
3994                    "couponDate": item["couponDate"],
3995                    "figi": bond[1]["figi"],
3996                    "ticker": bond[1]["ticker"],
3997                    "name": bond[1]["name"],
3998                    "couponNumber": item["couponNumber"],
3999                    "payOneBond": item["payOneBond"],
4000                    "payCurrency": item["payCurrency"],
4001                    "couponType": item["couponType"],
4002                    "couponPeriod": item["couponPeriod"],
4003                    "fixDate": item["fixDate"],
4004                    "couponStartDate": item["couponStartDate"],
4005                    "couponEndDate": item["couponEndDate"],
4006                }
4007
4008                if calendar is None:
4009                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4010
4011                else:
4012                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4013
4014        calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4015
4016        # Saving calendar from pandas dataframe to XLSX sheet:
4017        if xlsx:
4018            xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4019
4020            with pd.ExcelWriter(
4021                    path=xlsxCalendarFile,
4022                    date_format=TKS_DATE_FORMAT,
4023                    datetime_format=TKS_DATE_TIME_FORMAT,
4024                    mode="w",
4025            ) as writer:
4026                humanReadable = calendar.copy(deep=True)
4027                humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4028                humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4029                humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4030                humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4031                humanReadable.columns = colNames  # human-readable column names
4032
4033                humanReadable.to_excel(
4034                    writer,
4035                    sheet_name="Bond payments calendar",
4036                    index=False,
4037                    encoding="UTF-8",
4038                    freeze_panes=(1, 2),
4039                )  # saving as XLSX-file with freeze first row and column as headers
4040
4041                del humanReadable  # release df in memory
4042
4043            uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4044
4045        return calendar

Creates bond payments calendar as pandas dataframe, and also save it to the XLSX-file, calendar.xlsx by default.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowBondsCalendar(), ExtendBondsData().

Parameters
  • extBonds: pandas dataframe object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • xlsx: if True then also exports pandas dataframe to file calendarFile + ".xlsx", calendar.xlsx by default, for further used by data scientists or stock analytics.
Returns

pandas dataframe with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon

def ShowBondsCalendar(self, extBonds: pandas.core.frame.DataFrame, show: bool = True) -> str:
4047    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4048        """
4049        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4050        Also, creates Markdown file with calendar data, `calendar.md` by default.
4051
4052        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4053
4054        :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains
4055                        extended information about bonds: main info, current prices, bond payment calendar,
4056                        coupon yields, current yields and some statistics etc.
4057                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4058        :param show: if `True` then also printing bonds payment calendar to the console,
4059                     otherwise save to file `calendarFile` only. `False` by default.
4060        :return: multilines text in Markdown format with bonds payment calendar as a table.
4061        """
4062        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4063            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4064
4065        infoText = "# Bond payments calendar\n\n"
4066
4067        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate pandas dataframe with full calendar data
4068
4069        if not calendar.empty:
4070            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4071
4072            info = [
4073                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4074                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4075            ]
4076
4077            newMonth = False
4078            notOneBond = calendar["figi"].nunique() > 1
4079            for i, bond in enumerate(calendar.iterrows()):
4080                if newMonth and notOneBond:
4081                    info.append(splitLine)
4082
4083                info.append(
4084                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4085                        "  +" if bond[1]["paid"] else "  —",
4086                        bond[1]["couponDate"].split("T")[0],
4087                        bond[1]["figi"],
4088                        bond[1]["ticker"],
4089                        bond[1]["couponNumber"],
4090                        "{} {}".format(
4091                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4092                            bond[1]["payCurrency"],
4093                        ),
4094                        bond[1]["couponType"],
4095                        bond[1]["couponPeriod"],
4096                        bond[1]["fixDate"].split("T")[0],
4097                    )
4098                )
4099
4100                if i < len(calendar.values) - 1:
4101                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4102                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4103                    newMonth = False if curDate.month == nextDate.month else True
4104
4105                else:
4106                    newMonth = False
4107
4108            infoText += "".join(info)
4109
4110            if show:
4111                uLogger.info("{}".format(infoText))
4112
4113            if self.calendarFile is not None:
4114                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4115                    fH.write(infoText)
4116
4117                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4118
4119        else:
4120            infoText += "No data\n"
4121
4122        return infoText

Show bond payments calendar as a table. One row in input bonds dataframe contains one bond. Also, creates Markdown file with calendar data, calendar.md by default.

See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().

Parameters
  • extBonds: pandas dataframe object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • show: if True then also printing bonds payment calendar to the console, otherwise save to file calendarFile only. False by default.
Returns

multilines text in Markdown format with bonds payment calendar as a table.

def OverviewAccounts(self, show: bool = False) -> dict:
4124    def OverviewAccounts(self, show: bool = False) -> dict:
4125        """
4126        Method for parsing and show simple table with all available user accounts.
4127
4128        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4129
4130        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4131        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4132                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4133                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4134                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4135                                                        "closed": "—", "access": "Full access" }, ...}}`
4136        """
4137        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4138
4139        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4140        accounts = {
4141            item["id"]: {
4142                "type": TKS_ACCOUNT_TYPES[item["type"]],
4143                "name": item["name"],
4144                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4145                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4146                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4147                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4148            } for item in rawAccounts["accounts"]
4149        }
4150
4151        # Raw and parsed data with some fields replaced in "stat" section:
4152        view = {
4153            "rawAccounts": rawAccounts,
4154            "stat": accounts,
4155        }
4156
4157        # --- Prepare simple text table with only accounts data in human-readable format:
4158        if show:
4159            info = [
4160                "# User accounts\n\n",
4161                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4162                "| Account ID   | Type                      | Status                    | Name                           |\n",
4163                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4164            ]
4165
4166            for account in view["stat"].keys():
4167                info.extend([
4168                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4169                        account,
4170                        view["stat"][account]["type"],
4171                        view["stat"][account]["status"],
4172                        view["stat"][account]["name"],
4173                    )
4174                ])
4175
4176            infoText = "".join(info)
4177
4178            uLogger.info(infoText)
4179
4180            if self.userAccountsFile:
4181                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4182                    fH.write(infoText)
4183
4184                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4185
4186        return view

Method for parsing and show simple table with all available user accounts.

See also: RequestAccounts() and OverviewUserInfo() methods.

Parameters
  • show: if False then only dictionary with accounts data returns, if True then also print it to log.
Returns

dict with parsed accounts data received from RequestAccounts() method. Example of dict: view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}

def OverviewUserInfo(self, show: bool = False) -> dict:
4188    def OverviewUserInfo(self, show: bool = False) -> dict:
4189        """
4190        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4191
4192        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4193
4194        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4195        :return: dict with raw parsed data from server and some calculated statistics about it.
4196        """
4197        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4198        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4199        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4200        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4201        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4202        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4203
4204        # This is dict with parsed common user data:
4205        userInfo = {
4206            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4207            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4208            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4209            "tariff": rawUserInfo["tariff"],
4210        }
4211
4212        # This is an array of dict with parsed margin statuses for every account IDs:
4213        margins = {}
4214        for accountId in accounts.keys():
4215            if rawMargins[accountId]:
4216                margins[accountId] = {
4217                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4218                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4219                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4220                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4221                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4222                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4223                }
4224
4225            else:
4226                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4227
4228        unary = {}  # unary-connection limits
4229        for item in rawTariffLimits["unaryLimits"]:
4230            if item["limitPerMinute"] in unary.keys():
4231                unary[item["limitPerMinute"]].extend(item["methods"])
4232
4233            else:
4234                unary[item["limitPerMinute"]] = item["methods"]
4235
4236        stream = {}  # stream-connection limits
4237        for item in rawTariffLimits["streamLimits"]:
4238            if item["limit"] in stream.keys():
4239                stream[item["limit"]].extend(item["streams"])
4240
4241            else:
4242                stream[item["limit"]] = item["streams"]
4243
4244        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4245        limits = {
4246            "unary": unary,
4247            "stream": stream,
4248        }
4249
4250        # Raw and parsed data as an output result:
4251        view = {
4252            "rawUserInfo": rawUserInfo,
4253            "rawAccounts": rawAccounts,
4254            "rawMargins": rawMargins,
4255            "rawTariffLimits": rawTariffLimits,
4256            "stat": {
4257                "userInfo": userInfo,
4258                "accounts": accounts,
4259                "margins": margins,
4260                "limits": limits,
4261            },
4262        }
4263
4264        # --- Prepare text table with user information in human-readable format:
4265        if show:
4266            info = [
4267                "# Full user information\n\n",
4268                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4269                "## Common information\n\n",
4270                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4271                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4272                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4273                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4274                "\n## User accounts\n\n",
4275            ]
4276
4277            for account in view["stat"]["accounts"].keys():
4278                info.extend([
4279                    "### ID: [{}]\n\n".format(account),
4280                    "| Parameters           | Values                                                       |\n",
4281                    "|----------------------|--------------------------------------------------------------|\n",
4282                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4283                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4284                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4285                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4286                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4287                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4288                ])
4289
4290                if margins[account]:
4291                    info.extend([
4292                        "| Margin status:       | Enabled                                                      |\n",
4293                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4294                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4295                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4296                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4297                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4298                    ])
4299
4300                else:
4301                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4302
4303            info.extend([
4304                "\n## Current user tariff limits\n",
4305                "\nSee also:\n",
4306                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4307                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4308                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4309                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4310                "\n### Unary limits\n",
4311            ])
4312
4313            if unary:
4314                for key, values in sorted(unary.items()):
4315                    info.append("\n* Max requests per minute: {}\n".format(key))
4316
4317                    for value in values:
4318                        info.append("  - {}\n".format(value))
4319
4320            else:
4321                info.append("\nNot available\n")
4322
4323            info.append("\n### Stream limits\n")
4324
4325            if stream:
4326                for key, values in sorted(stream.items()):
4327                    info.append("\n* Max stream connections: {}\n".format(key))
4328
4329                    for value in values:
4330                        info.append("  - {}\n".format(value))
4331
4332            else:
4333                info.append("\nNot available\n")
4334
4335            infoText = "".join(info)
4336
4337            uLogger.info(infoText)
4338
4339            if self.userInfoFile:
4340                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4341                    fH.write(infoText)
4342
4343                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4344
4345        return view

Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).

See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.

Parameters
  • show: if False then only dictionary returns, if True then also print user's data to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

class Args:
4348class Args:
4349    """
4350    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4351    """
4352    def __init__(self, **kwargs):
4353        self.__dict__.update(kwargs)
4354
4355    def __getattr__(self, item):
4356        return None

If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.

Args(**kwargs)
4352    def __init__(self, **kwargs):
4353        self.__dict__.update(kwargs)
def ParseArgs()
4359def ParseArgs():
4360    """
4361    Function get and parse command line keys.
4362
4363    See examples:
4364    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4365    - in russian: https://tim55667757.github.io/TKSBrokerAPI/
4366    """
4367    parser = ArgumentParser()  # command-line string parser
4368
4369    parser.description = "TKSBrokerAPI is a python API to work with some methods of Tinkoff Open API using REST protocol. It can view history, orders and market information. Also, you can open orders and trades. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples"
4370    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4371
4372    # --- options:
4373
4374    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the program. `False` by default.")
4375    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4376    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4377
4378    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4379    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4380
4381    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4382    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4383
4384    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4385
4386    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4387    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4388    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4389
4390    parser.add_argument("--debug-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4391
4392    # --- commands:
4393
4394    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4395
4396    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4397    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4398    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider pandas dataframe with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4399    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4400    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4401    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4402    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4403    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4404
4405    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4406    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4407    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4408    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4409    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4410
4411    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4412    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4413    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4414    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4415
4416    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4417    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4418    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4419
4420    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4421    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4422    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4423    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4424    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4425    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4426    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4427
4428    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4429    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4430    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` key, including for currencies tickers.")
4431    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers, including for currencies tickers.")
4432    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.")
4433
4434    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4435    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4436    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4437
4438    cmdArgs = parser.parse_args()
4439    return cmdArgs

Function get and parse command line keys.

See examples:

def Main(**kwargs)
4442def Main(**kwargs):
4443    """
4444    Main function for work with Tinkoff Open API service. It realizes simple logic: get a lot of options and execute one command.
4445
4446    See examples:
4447    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4448    - in russian: https://tim55667757.github.io/TKSBrokerAPI/
4449    """
4450    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4451
4452    if args.debug_level:
4453        uLogger.level = 10  # always debug level by default
4454        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4455
4456    exitCode = 0
4457    start = datetime.now(tzutc())
4458    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4459        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4460        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4461    ))
4462
4463    # trying to calculate full current version:
4464    buildVersion = __version__
4465    try:
4466        v = version("tksbrokerapi")
4467        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4468
4469    except Exception:
4470        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4471
4472    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4473    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4474
4475    try:
4476        if args.version:
4477            print("TKSBrokerAPI {}".format(buildVersion))
4478            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4479
4480        else:
4481            # Init class for trading with Tinkoff Broker: TODO: rename `server` to `trader`
4482            server = TinkoffBrokerServer(
4483                token=args.token,
4484                accountId=args.account_id,
4485                useCache=not args.no_cache,
4486            )
4487
4488            # --- set some options:
4489
4490            if args.ticker:
4491                if args.ticker in server.aliasesKeys:
4492                    server.ticker = server.aliases[args.ticker]  # Replace some tickers with its aliases
4493
4494                else:
4495                    server.ticker = args.ticker
4496
4497            if args.figi:
4498                server.figi = args.figi
4499
4500            if args.depth is not None:
4501                server.depth = args.depth
4502
4503            # --- do one of commands:
4504
4505            if args.list:
4506                if args.output is not None:
4507                    server.instrumentsFile = args.output
4508
4509                server.ShowInstrumentsInfo(show=True)
4510
4511            elif args.list_xlsx:
4512                server.DumpInstrumentsAsXLSX(forceUpdate=False)
4513
4514            elif args.bonds_xlsx is not None:
4515                if args.output is not None:
4516                    server.bondsXLSXFile = args.output
4517
4518                if len(args.bonds_xlsx) == 0:
4519                    server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4520
4521                else:
4522                    server.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4523
4524            elif args.search:
4525                if args.output is not None:
4526                    server.searchResultsFile = args.output
4527
4528                server.SearchInstruments(pattern=args.search[0], show=True)
4529
4530            elif args.info:
4531                if not (args.ticker or args.figi):
4532                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4533                    raise Exception("Ticker or FIGI required")
4534
4535                if args.output is not None:
4536                    server.infoFile = args.output
4537
4538                if args.ticker:
4539                    server.SearchByTicker(requestPrice=True, show=True, debug=False)  # show info and current prices by ticker name
4540
4541                else:
4542                    server.SearchByFIGI(requestPrice=True, show=True, debug=False)  # show info and current prices by FIGI id
4543
4544            elif args.calendar is not None:
4545                if args.output is not None:
4546                    server.calendarFile = args.output
4547
4548                if len(args.calendar) == 0:
4549                    bondsData = server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4550
4551                else:
4552                    bondsData = server.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4553
4554                server.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4555
4556            elif args.price:
4557                if not (args.ticker or args.figi):
4558                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4559                    raise Exception("Ticker or FIGI required")
4560
4561                server.GetCurrentPrices(show=True)
4562
4563            elif args.prices is not None:
4564                if args.output is not None:
4565                    server.pricesFile = args.output
4566
4567                server.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4568
4569            elif args.overview:
4570                if args.output is not None:
4571                    server.overviewFile = args.output
4572
4573                server.Overview(show=True, details="full")
4574
4575            elif args.overview_digest:
4576                if args.output is not None:
4577                    server.overviewDigestFile = args.output
4578
4579                server.Overview(show=True, details="digest")
4580
4581            elif args.overview_positions:
4582                if args.output is not None:
4583                    server.overviewPositionsFile = args.output
4584
4585                server.Overview(show=True, details="positions")
4586
4587            elif args.overview_orders:
4588                if args.output is not None:
4589                    server.overviewOrdersFile = args.output
4590
4591                server.Overview(show=True, details="orders")
4592
4593            elif args.overview_analytics:
4594                if args.output is not None:
4595                    server.overviewAnalyticsFile = args.output
4596
4597                server.Overview(show=True, details="analytics")
4598
4599            elif args.deals is not None:
4600                if args.output is not None:
4601                    server.reportFile = args.output
4602
4603                if 0 <= len(args.deals) < 3:
4604                    server.Deals(
4605                        start=args.deals[0] if len(args.deals) >= 1 else None,
4606                        end=args.deals[1] if len(args.deals) == 2 else None,
4607                        show=True,  # Always show deals report in console
4608                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4609                    )
4610
4611                else:
4612                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4613                    raise Exception("Incorrect value")
4614
4615            elif args.history is not None:
4616                if args.output is not None:
4617                    server.historyFile = args.output
4618
4619                if 0 <= len(args.history) < 3:
4620                    dataReceived = server.History(
4621                        start=args.history[0] if len(args.history) >= 1 else None,
4622                        end=args.history[1] if len(args.history) == 2 else None,
4623                        interval="hour" if args.interval is None or not args.interval else args.interval,
4624                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4625                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4626                        show=True,  # shows all downloaded candles in console
4627                    )
4628
4629                    if args.render_chart is not None and dataReceived is not None:
4630                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4631
4632                        server.ShowHistoryChart(
4633                            candles=dataReceived,
4634                            interact=iChart,
4635                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4636                        )
4637
4638                else:
4639                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4640                    raise Exception("Incorrect value")
4641
4642            elif args.load_history is not None:
4643                histData = server.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
4644
4645                if args.render_chart is not None and histData is not None:
4646                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4647                    server.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
4648
4649                    server.ShowHistoryChart(
4650                        candles=histData,
4651                        interact=iChart,
4652                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4653                    )
4654
4655            elif args.trade is not None:
4656                if 1 <= len(args.trade) <= 5:
4657                    server.Trade(
4658                        operation=args.trade[0],
4659                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
4660                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
4661                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
4662                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
4663                    )
4664
4665                else:
4666                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4667
4668            elif args.buy is not None:
4669                if 0 <= len(args.buy) <= 4:
4670                    server.Buy(
4671                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
4672                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
4673                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
4674                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
4675                    )
4676
4677                else:
4678                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4679
4680            elif args.sell is not None:
4681                if 0 <= len(args.sell) <= 4:
4682                    server.Sell(
4683                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
4684                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
4685                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
4686                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
4687                    )
4688
4689                else:
4690                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4691
4692            elif args.order:
4693                if 4 <= len(args.order) <= 7:
4694                    server.Order(
4695                        operation=args.order[0],
4696                        orderType=args.order[1],
4697                        lots=int(args.order[2]),
4698                        targetPrice=float(args.order[3]),
4699                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
4700                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
4701                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
4702                    )
4703
4704                else:
4705                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
4706
4707            elif args.buy_limit:
4708                server.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
4709
4710            elif args.sell_limit:
4711                server.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
4712
4713            elif args.buy_stop:
4714                if 2 <= len(args.buy_stop) <= 7:
4715                    server.BuyStop(
4716                        lots=int(args.buy_stop[0]),
4717                        targetPrice=float(args.buy_stop[1]),
4718                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
4719                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
4720                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
4721                    )
4722
4723                else:
4724                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4725
4726            elif args.sell_stop:
4727                if 2 <= len(args.sell_stop) <= 7:
4728                    server.SellStop(
4729                        lots=int(args.sell_stop[0]),
4730                        targetPrice=float(args.sell_stop[1]),
4731                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
4732                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
4733                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
4734                    )
4735
4736                else:
4737                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
4738
4739            # elif args.buy_order_grid is not None:
4740            #     # update order grid work with api v2
4741            #     if len(args.buy_order_grid) == 2:
4742            #         orderParams = server.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
4743            #
4744            #         for order in orderParams:
4745            #             server.Order(operation="Buy", lots=order["lot"], price=order["price"])
4746            #
4747            #     else:
4748            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4749            #
4750            # elif args.sell_order_grid is not None:
4751            #     # update order grid work with api v2
4752            #     if len(args.sell_order_grid) >= 2:
4753            #         orderParams = server.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
4754            #
4755            #         for order in orderParams:
4756            #             server.Order(operation="Sell", lots=order["lot"], price=order["price"])
4757            #
4758            #     else:
4759            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4760
4761            elif args.close_order is not None:
4762                server.CloseOrders(args.close_order)  # close only one order
4763
4764            elif args.close_orders is not None:
4765                server.CloseOrders(args.close_orders)  # close list of orders
4766
4767            elif args.close_trade:
4768                if not args.ticker:
4769                    uLogger.error("`--ticker` key is required for this operation!")
4770                    raise Exception("Ticker required")
4771
4772                server.CloseTrades([args.ticker])  # close only one trade
4773
4774            elif args.close_trades is not None:
4775                server.CloseTrades(args.close_trades)  # close trades for list of tickers
4776
4777            elif args.close_all is not None:
4778                server.CloseAll(*args.close_all)
4779
4780            elif args.limits:
4781                if args.output is not None:
4782                    server.withdrawalLimitsFile = args.output
4783
4784                server.OverviewLimits(show=True)
4785
4786            elif args.user_info:
4787                if args.output is not None:
4788                    server.userInfoFile = args.output
4789
4790                server.OverviewUserInfo(show=True)
4791
4792            elif args.account:
4793                if args.output is not None:
4794                    server.userAccountsFile = args.output
4795
4796                server.OverviewAccounts(show=True)
4797
4798            else:
4799                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
4800                raise Exception("There is no command to execute")
4801
4802    except Exception:
4803        trace = tb.format_exc()
4804        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
4805            if e in trace:
4806                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
4807                break
4808
4809        uLogger.debug(trace)
4810        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
4811        exitCode = 255  # an error occurred, must be open a ticket for this issue
4812
4813    finally:
4814        finish = datetime.now(tzutc())
4815
4816        if exitCode == 0:
4817            uLogger.debug("All operations were finished success (summary code is 0).")
4818
4819        else:
4820            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
4821                os.path.abspath(uLog.defaultLogFile), exitCode,
4822            ))
4823
4824        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
4825        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
4826            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4827            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4828        ))
4829
4830        if not kwargs:
4831            sys.exit(exitCode)
4832
4833        else:
4834            return exitCode

Main function for work with Tinkoff Open API service. It realizes simple logic: get a lot of options and execute one command.

See examples: